mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-09 14:54:04 +02:00
Add the possibility to save added annotations when reorganizing a pdf (bug 2023086)
This commit is contained in:
parent
ff1af5a058
commit
04272de41d
@ -354,12 +354,12 @@ class AnnotationFactory {
|
||||
|
||||
static async saveNewAnnotations(
|
||||
evaluator,
|
||||
xref,
|
||||
task,
|
||||
annotations,
|
||||
imagePromises,
|
||||
changes
|
||||
) {
|
||||
const xref = evaluator.xref;
|
||||
let baseFontRef;
|
||||
const promises = [];
|
||||
const { isOffscreenCanvasSupported } = evaluator.options;
|
||||
|
||||
@ -151,6 +151,10 @@ class Page {
|
||||
});
|
||||
}
|
||||
|
||||
createAnnotationEvaluator(handler) {
|
||||
return this.#createPartialEvaluator(handler);
|
||||
}
|
||||
|
||||
#getInheritableProperty(key, getArray = false) {
|
||||
const value = getInheritableProperty({
|
||||
dict: this.pageDict,
|
||||
@ -386,6 +390,7 @@ class Page {
|
||||
);
|
||||
const newData = await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
this.xref,
|
||||
task,
|
||||
annotations,
|
||||
imagePromises,
|
||||
|
||||
@ -16,16 +16,21 @@
|
||||
/** @typedef {import("../document.js").PDFDocument} PDFDocument */
|
||||
/** @typedef {import("../document.js").Page} Page */
|
||||
/** @typedef {import("../xref.js").XRef} XRef */
|
||||
/** @typedef {import("../worker.js").WorkerTask} WorkerTask */
|
||||
// eslint-disable-next-line max-len
|
||||
/** @typedef {import("../../shared/message_handler.js").MessageHandler} MessageHandler */
|
||||
|
||||
import {
|
||||
deepCompare,
|
||||
getInheritableProperty,
|
||||
getNewAnnotationsMap,
|
||||
stringToAsciiOrUTF16BE,
|
||||
} from "../core_utils.js";
|
||||
import { Dict, isName, Name, Ref, RefSet, RefSetCache } from "../primitives.js";
|
||||
import { getModificationDate, stringToPDFString } from "../../shared/util.js";
|
||||
import { incrementalUpdate, writeValue } from "../writer.js";
|
||||
import { NameTree, NumberTree } from "../name_number_tree.js";
|
||||
import { AnnotationFactory } from "../annotation.js";
|
||||
import { BaseStream } from "../base_stream.js";
|
||||
import { StringStream } from "../stream.js";
|
||||
|
||||
@ -75,8 +80,9 @@ class DocumentData {
|
||||
}
|
||||
|
||||
class XRefWrapper {
|
||||
constructor(entries) {
|
||||
constructor(entries, getNewRef) {
|
||||
this.entries = entries;
|
||||
this._getNewRef = getNewRef;
|
||||
}
|
||||
|
||||
fetch(ref) {
|
||||
@ -94,11 +100,17 @@ class XRefWrapper {
|
||||
fetchAsync(ref) {
|
||||
return Promise.resolve(this.fetch(ref));
|
||||
}
|
||||
|
||||
getNewTemporaryRef() {
|
||||
return this._getNewRef();
|
||||
}
|
||||
}
|
||||
|
||||
class PDFEditor {
|
||||
hasSingleFile = false;
|
||||
|
||||
#newAnnotationsParams = null;
|
||||
|
||||
currentDocument = null;
|
||||
|
||||
oldPages = [];
|
||||
@ -107,7 +119,7 @@ class PDFEditor {
|
||||
|
||||
xref = [null];
|
||||
|
||||
xrefWrapper = new XRefWrapper(this.xref);
|
||||
xrefWrapper = new XRefWrapper(this.xref, () => this.newRef);
|
||||
|
||||
newRefCount = 1;
|
||||
|
||||
@ -535,13 +547,33 @@ class PDFEditor {
|
||||
/**
|
||||
* Extract pages from the given documents.
|
||||
* @param {Array<PageInfo>} pageInfos
|
||||
* @param {Object} annotationStorage - The annotation storage containing the
|
||||
* annotations to be merged into the new document.
|
||||
* @param {MessageHandler} handler - The message handler to use for processing
|
||||
* the annotations.
|
||||
* @param {WorkerTask} task - The worker task to use for reporting progress
|
||||
* and cancellation.
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async extractPages(pageInfos) {
|
||||
async extractPages(pageInfos, annotationStorage, handler, task) {
|
||||
const promises = [];
|
||||
let newIndex = 0;
|
||||
this.hasSingleFile = pageInfos.length === 1;
|
||||
const allDocumentData = [];
|
||||
|
||||
if (annotationStorage) {
|
||||
this.#newAnnotationsParams = {
|
||||
handler,
|
||||
task,
|
||||
newAnnotationsByPage: getNewAnnotationsMap(annotationStorage),
|
||||
imagesPromises: AnnotationFactory.generateImages(
|
||||
annotationStorage.values(),
|
||||
this.xrefWrapper,
|
||||
true
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
for (const {
|
||||
document,
|
||||
includePages,
|
||||
@ -1930,6 +1962,8 @@ class PDFEditor {
|
||||
await this.#collectDependencies(resources, true, xref)
|
||||
);
|
||||
|
||||
let newAnnots = null;
|
||||
|
||||
if (annotations) {
|
||||
const newAnnotations = await this.#collectDependencies(
|
||||
annotations,
|
||||
@ -1937,9 +1971,35 @@ class PDFEditor {
|
||||
xref
|
||||
);
|
||||
this.#fixNamedDestinations(newAnnotations, dedupNamedDestinations);
|
||||
pageDict.setIfArray("Annots", newAnnotations);
|
||||
if (Array.isArray(newAnnotations) && newAnnotations.length > 0) {
|
||||
newAnnots = newAnnotations;
|
||||
}
|
||||
}
|
||||
|
||||
const newAnnotations =
|
||||
this.#newAnnotationsParams?.newAnnotationsByPage.get(pageIndex);
|
||||
if (newAnnotations) {
|
||||
const { handler, task, imagesPromises } = this.#newAnnotationsParams;
|
||||
const changes = new RefSetCache();
|
||||
const newData = await AnnotationFactory.saveNewAnnotations(
|
||||
page.createAnnotationEvaluator(handler),
|
||||
this.xrefWrapper,
|
||||
task,
|
||||
newAnnotations,
|
||||
imagesPromises,
|
||||
changes
|
||||
);
|
||||
for (const [ref, { data }] of changes.items()) {
|
||||
this.xref[ref.num] = data;
|
||||
}
|
||||
newAnnots ||= [];
|
||||
for (const { ref } of newData.annotations) {
|
||||
newAnnots.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
pageDict.setIfArray("Annots", newAnnots);
|
||||
|
||||
if (this.useObjectStreams) {
|
||||
const newLastRef = this.newRefCount;
|
||||
const pageObjectRefs = [];
|
||||
|
||||
@ -551,96 +551,111 @@ class WorkerMessageHandler {
|
||||
return pdfManager.ensureDoc("calculationOrderIds");
|
||||
});
|
||||
|
||||
handler.on("ExtractPages", async function ({ pageInfos }) {
|
||||
if (!pageInfos) {
|
||||
warn("extractPages: nothing to extract.");
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(pageInfos)) {
|
||||
pageInfos = [pageInfos];
|
||||
}
|
||||
let newDocumentId = 0;
|
||||
for (const pageInfo of pageInfos) {
|
||||
if (pageInfo.document === null) {
|
||||
pageInfo.document = pdfManager.pdfDocument;
|
||||
} else if (ArrayBuffer.isView(pageInfo.document)) {
|
||||
const manager = new LocalPdfManager({
|
||||
source: pageInfo.document,
|
||||
docId: `${docId}_extractPages_${newDocumentId++}`,
|
||||
handler,
|
||||
password: pageInfo.password ?? null,
|
||||
evaluatorOptions: Object.assign({}, pdfManager.evaluatorOptions),
|
||||
});
|
||||
let recoveryMode = false;
|
||||
let isValid = true;
|
||||
while (true) {
|
||||
try {
|
||||
await manager.requestLoadedStream();
|
||||
await manager.ensureDoc("checkHeader");
|
||||
await manager.ensureDoc("parseStartXRef");
|
||||
await manager.ensureDoc("parse", [recoveryMode]);
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e instanceof XRefParseException) {
|
||||
if (recoveryMode === false) {
|
||||
recoveryMode = true;
|
||||
continue;
|
||||
handler.on(
|
||||
"ExtractPages",
|
||||
async function ({ pageInfos, annotationStorage }) {
|
||||
if (!pageInfos) {
|
||||
warn("extractPages: nothing to extract.");
|
||||
return null;
|
||||
}
|
||||
if (!Array.isArray(pageInfos)) {
|
||||
pageInfos = [pageInfos];
|
||||
}
|
||||
let newDocumentId = 0;
|
||||
for (const pageInfo of pageInfos) {
|
||||
if (pageInfo.document === null) {
|
||||
pageInfo.document = pdfManager.pdfDocument;
|
||||
} else if (ArrayBuffer.isView(pageInfo.document)) {
|
||||
const manager = new LocalPdfManager({
|
||||
source: pageInfo.document,
|
||||
docId: `${docId}_extractPages_${newDocumentId++}`,
|
||||
handler,
|
||||
password: pageInfo.password ?? null,
|
||||
evaluatorOptions: Object.assign({}, pdfManager.evaluatorOptions),
|
||||
});
|
||||
let recoveryMode = false;
|
||||
let isValid = true;
|
||||
while (true) {
|
||||
try {
|
||||
await manager.requestLoadedStream();
|
||||
await manager.ensureDoc("checkHeader");
|
||||
await manager.ensureDoc("parseStartXRef");
|
||||
await manager.ensureDoc("parse", [recoveryMode]);
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e instanceof XRefParseException) {
|
||||
if (recoveryMode === false) {
|
||||
recoveryMode = true;
|
||||
continue;
|
||||
} else {
|
||||
isValid = false;
|
||||
warn("extractPages: XRefParseException.");
|
||||
}
|
||||
} else if (e instanceof PasswordException) {
|
||||
const task = new WorkerTask(
|
||||
`PasswordException: response ${e.code}`
|
||||
);
|
||||
|
||||
startWorkerTask(task);
|
||||
|
||||
try {
|
||||
const { password } = await handler.sendWithPromise(
|
||||
"PasswordRequest",
|
||||
e
|
||||
);
|
||||
manager.updatePassword(password);
|
||||
} catch {
|
||||
isValid = false;
|
||||
warn("extractPages: invalid password.");
|
||||
} finally {
|
||||
finishWorkerTask(task);
|
||||
}
|
||||
} else {
|
||||
isValid = false;
|
||||
warn("extractPages: XRefParseException.");
|
||||
warn("extractPages: invalid document.");
|
||||
}
|
||||
} else if (e instanceof PasswordException) {
|
||||
const task = new WorkerTask(
|
||||
`PasswordException: response ${e.code}`
|
||||
);
|
||||
|
||||
startWorkerTask(task);
|
||||
|
||||
try {
|
||||
const { password } = await handler.sendWithPromise(
|
||||
"PasswordRequest",
|
||||
e
|
||||
);
|
||||
manager.updatePassword(password);
|
||||
} catch {
|
||||
isValid = false;
|
||||
warn("extractPages: invalid password.");
|
||||
} finally {
|
||||
finishWorkerTask(task);
|
||||
if (!isValid) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
isValid = false;
|
||||
warn("extractPages: invalid document.");
|
||||
}
|
||||
if (!isValid) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isValid) {
|
||||
pageInfo.document = null;
|
||||
}
|
||||
const isPureXfa = await manager.ensureDoc("isPureXfa");
|
||||
if (isPureXfa) {
|
||||
pageInfo.document = null;
|
||||
warn("extractPages does not support pure XFA documents.");
|
||||
if (!isValid) {
|
||||
pageInfo.document = null;
|
||||
}
|
||||
const isPureXfa = await manager.ensureDoc("isPureXfa");
|
||||
if (isPureXfa) {
|
||||
pageInfo.document = null;
|
||||
warn("extractPages does not support pure XFA documents.");
|
||||
} else {
|
||||
pageInfo.document = manager.pdfDocument;
|
||||
}
|
||||
} else {
|
||||
pageInfo.document = manager.pdfDocument;
|
||||
warn("extractPages: invalid document.");
|
||||
}
|
||||
}
|
||||
let task;
|
||||
try {
|
||||
const pdfEditor = new PDFEditor();
|
||||
task = new WorkerTask(`ExtractPages: ${pageInfos.length} page(s)`);
|
||||
startWorkerTask(task);
|
||||
const buffer = await pdfEditor.extractPages(
|
||||
pageInfos,
|
||||
annotationStorage,
|
||||
handler,
|
||||
task
|
||||
);
|
||||
return buffer;
|
||||
} catch (reason) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(reason);
|
||||
return null;
|
||||
} finally {
|
||||
if (task) {
|
||||
finishWorkerTask(task);
|
||||
}
|
||||
} else {
|
||||
warn("extractPages: invalid document.");
|
||||
}
|
||||
}
|
||||
try {
|
||||
const pdfEditor = new PDFEditor();
|
||||
const buffer = await pdfEditor.extractPages(pageInfos);
|
||||
return buffer;
|
||||
} catch (reason) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(reason);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
handler.on(
|
||||
"SaveDocument",
|
||||
|
||||
@ -2973,7 +2973,20 @@ class WorkerTransport {
|
||||
}
|
||||
|
||||
extractPages(pageInfos) {
|
||||
return this.messageHandler.sendWithPromise("ExtractPages", { pageInfos });
|
||||
const params = {
|
||||
pageInfos,
|
||||
};
|
||||
let transfer;
|
||||
if (this.annotationStorage.size > 0) {
|
||||
const { map, transfer: t } = this.annotationStorage.serializable;
|
||||
params.annotationStorage = map;
|
||||
transfer = t;
|
||||
}
|
||||
return this.messageHandler
|
||||
.sendWithPromise("ExtractPages", params, transfer)
|
||||
.finally(() => {
|
||||
this.annotationStorage.resetModified();
|
||||
});
|
||||
}
|
||||
|
||||
getPage(pageNumber) {
|
||||
|
||||
@ -144,47 +144,6 @@ class AnnotationEditorLayer {
|
||||
this.#uiManager.addLayer(this);
|
||||
}
|
||||
|
||||
updatePageIndex(newPageIndex) {
|
||||
for (const editor of this.#allEditorsIterator) {
|
||||
editor.updatePageIndex(newPageIndex);
|
||||
}
|
||||
|
||||
this.pageIndex = newPageIndex;
|
||||
this.#uiManager.addLayer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones all annotation editors from another layer into this layer.
|
||||
* This is typically used when duplicating a page - the editors from the
|
||||
* source page are serialized and then deserialized into the new page's layer.
|
||||
*
|
||||
* @param {AnnotationEditorLayer} clonedFrom - The source annotation editor
|
||||
* layer to clone editors from. If null or undefined, no action is taken.
|
||||
* @returns {Promise<void>} A promise that resolves when all editors have been
|
||||
* cloned and added to this layer.
|
||||
*/
|
||||
async setClonedFrom(clonedFrom) {
|
||||
if (!clonedFrom) {
|
||||
return;
|
||||
}
|
||||
const promises = [];
|
||||
for (const editor of clonedFrom.#allEditorsIterator) {
|
||||
const serialized = editor.serialize(/* isForCopying = */ true);
|
||||
if (!serialized) {
|
||||
continue;
|
||||
}
|
||||
serialized.isCopy = false;
|
||||
promises.push(
|
||||
this.deserialize(serialized).then(deserialized => {
|
||||
if (deserialized) {
|
||||
this.addOrRebuild(deserialized);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this.#editors.size === 0;
|
||||
}
|
||||
@ -713,14 +672,6 @@ class AnnotationEditorLayer {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an id for an editor.
|
||||
* @returns {string}
|
||||
*/
|
||||
getNextId() {
|
||||
return this.#uiManager.getId();
|
||||
}
|
||||
|
||||
get #currentEditorType() {
|
||||
return AnnotationEditorLayer.#editorTypes.get(this.#uiManager.getMode());
|
||||
}
|
||||
@ -753,7 +704,7 @@ class AnnotationEditorLayer {
|
||||
await this.#uiManager.updateMode(options.mode);
|
||||
|
||||
const { offsetX, offsetY } = this.#getCenterPoint();
|
||||
const id = this.getNextId();
|
||||
const id = this.#uiManager.getId();
|
||||
const editor = this.#createNewEditor({
|
||||
parent: this,
|
||||
id,
|
||||
@ -789,7 +740,7 @@ class AnnotationEditorLayer {
|
||||
* @returns {AnnotationEditor}
|
||||
*/
|
||||
createAndAddNewEditor(event, isCentered, data = {}) {
|
||||
const id = this.getNextId();
|
||||
const id = this.#uiManager.getId();
|
||||
const editor = this.#createNewEditor({
|
||||
parent: this,
|
||||
id,
|
||||
@ -1073,13 +1024,17 @@ class AnnotationEditorLayer {
|
||||
* Render the main editor.
|
||||
* @param {RenderEditorLayerOptions} parameters
|
||||
*/
|
||||
render({ viewport }) {
|
||||
async render({ viewport }) {
|
||||
this.viewport = viewport;
|
||||
setLayerDimensions(this.div, viewport);
|
||||
for (const editor of this.#uiManager.getEditors(this.pageIndex)) {
|
||||
this.add(editor);
|
||||
editor.rebuild();
|
||||
}
|
||||
|
||||
await this.#uiManager.findClonesForPage(this);
|
||||
this.div.hidden = this.isEmpty;
|
||||
|
||||
// We're maybe rendering a layer which was invisible when we started to edit
|
||||
// so we must set the different callbacks for it.
|
||||
this.updateMode();
|
||||
|
||||
@ -235,7 +235,7 @@ class AnnotationEditor {
|
||||
|
||||
static deleteAnnotationElement(editor) {
|
||||
const fakeEditor = new FakeEditor({
|
||||
id: editor.parent.getNextId(),
|
||||
id: editor._uiManager.getId(),
|
||||
parent: editor.parent,
|
||||
uiManager: editor._uiManager,
|
||||
});
|
||||
@ -480,6 +480,10 @@ class AnnotationEditor {
|
||||
}
|
||||
|
||||
_moveAfterPaste(baseX, baseY) {
|
||||
if (this.isClone) {
|
||||
delete this.isClone;
|
||||
return;
|
||||
}
|
||||
const [parentWidth, parentHeight] = this.parentDimensions;
|
||||
this.setAt(
|
||||
baseX * parentWidth,
|
||||
@ -1862,7 +1866,7 @@ class AnnotationEditor {
|
||||
static async deserialize(data, parent, uiManager) {
|
||||
const editor = new this.prototype.constructor({
|
||||
parent,
|
||||
id: parent.getNextId(),
|
||||
id: uiManager.getId(),
|
||||
uiManager,
|
||||
annotationElementId: data.annotationElementId,
|
||||
creationDate: data.creationDate,
|
||||
|
||||
@ -959,7 +959,7 @@ class HighlightEditor extends AnnotationEditor {
|
||||
};
|
||||
}
|
||||
|
||||
const { color, quadPoints, inkLists, opacity } = data;
|
||||
const { color, quadPoints, inkLists, outlines, opacity } = data;
|
||||
const editor = await super.deserialize(data, parent, uiManager);
|
||||
|
||||
editor.color = Util.makeHexColor(...color);
|
||||
@ -988,9 +988,9 @@ class HighlightEditor extends AnnotationEditor {
|
||||
editor.#createOutlines();
|
||||
editor.#addToDrawLayer();
|
||||
editor.rotate(editor.rotation);
|
||||
} else if (inkLists) {
|
||||
} else if (inkLists || outlines) {
|
||||
editor.#isFreeHighlight = true;
|
||||
const points = inkLists[0];
|
||||
const points = (inkLists || outlines.points)[0];
|
||||
const point = {
|
||||
x: points[0] - pageX,
|
||||
y: pageHeight - (points[1] - pageY),
|
||||
|
||||
@ -675,6 +675,8 @@ class AnnotationEditorUIManager {
|
||||
|
||||
#allLayers = new Map();
|
||||
|
||||
#savedAllLayers = null;
|
||||
|
||||
#altTextManager = null;
|
||||
|
||||
#annotationStorage = null;
|
||||
@ -1841,6 +1843,66 @@ class AnnotationEditorUIManager {
|
||||
}
|
||||
}
|
||||
|
||||
updatePageIndex(oldPageIndex, newPageIndex) {
|
||||
for (const editor of this.getEditors(oldPageIndex)) {
|
||||
editor.pageIndex = newPageIndex;
|
||||
}
|
||||
const layer = this.#savedAllLayers.get(oldPageIndex);
|
||||
if (layer) {
|
||||
layer.pageIndex = newPageIndex;
|
||||
this.#allLayers.set(newPageIndex, layer);
|
||||
if (this.#isEnabled) {
|
||||
layer.enable();
|
||||
} else {
|
||||
layer.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startUpdatePages() {
|
||||
this.#savedAllLayers = new Map(this.#allLayers);
|
||||
this.#allLayers.clear();
|
||||
}
|
||||
|
||||
endUpdatePages() {
|
||||
this.#savedAllLayers = null;
|
||||
}
|
||||
|
||||
clonePage(pageIndex, newPageIndex) {
|
||||
for (const editor of this.getEditors(pageIndex)) {
|
||||
const serialized = editor.serialize(
|
||||
editor.mode !== AnnotationEditorType.HIGHLIGHT
|
||||
);
|
||||
if (!serialized) {
|
||||
continue;
|
||||
}
|
||||
serialized.pageIndex = newPageIndex;
|
||||
serialized.id = this.getId();
|
||||
serialized.isClone = true;
|
||||
delete serialized.popupRef;
|
||||
this.#annotationStorage.setValue(serialized.id, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
findClonesForPage(layer) {
|
||||
const promises = [];
|
||||
const { pageIndex } = layer;
|
||||
for (const [id, editor] of this.#annotationStorage) {
|
||||
if (editor.pageIndex === pageIndex && editor.isClone) {
|
||||
this.#annotationStorage.remove(id);
|
||||
promises.push(
|
||||
layer.deserialize(editor).then(deserializedEditor => {
|
||||
if (deserializedEditor) {
|
||||
deserializedEditor.isClone = true;
|
||||
layer.addOrRebuild(deserializedEditor);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the different possible states of this manager, e.g. is there
|
||||
* something to undo, redo, ...
|
||||
|
||||
@ -31,9 +31,13 @@ import {
|
||||
PDI,
|
||||
scrollIntoView,
|
||||
showViewsManager,
|
||||
switchToEditor,
|
||||
waitAndClick,
|
||||
waitForBrowserTrip,
|
||||
waitForDOMMutation,
|
||||
waitForPointerUp,
|
||||
waitForSerialized,
|
||||
waitForStorageEntries,
|
||||
waitForTextToBe,
|
||||
waitForTooltipToBe,
|
||||
} from "./test_utils.mjs";
|
||||
@ -2563,4 +2567,87 @@ describe("Reorganize Pages View", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Copy page with an ink annotation and paste it", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"page_with_number.pdf",
|
||||
".annotationEditorLayer",
|
||||
"50",
|
||||
null,
|
||||
{ enableSplitMerge: true }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("should check that the pasted page has an ink annotation in the DOM", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
// Enable ink editor mode and draw a line on page 1.
|
||||
await switchToEditor("Ink", page);
|
||||
const rect = await getRect(
|
||||
page,
|
||||
".page[data-page-number='1'] .annotationEditorLayer"
|
||||
);
|
||||
const x = rect.x + rect.width * 0.3;
|
||||
const y = rect.y + rect.height * 0.3;
|
||||
const clickHandle = await waitForPointerUp(page);
|
||||
await page.mouse.move(x, y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(x + 50, y + 50);
|
||||
await page.mouse.up();
|
||||
await awaitPromise(clickHandle);
|
||||
|
||||
// Commit the drawing and wait for it to be serialized.
|
||||
await page.keyboard.press("Escape");
|
||||
await waitForSerialized(page, 1);
|
||||
|
||||
await waitForThumbnailVisible(page, 1);
|
||||
|
||||
// Select page 1 and copy it.
|
||||
await page.waitForSelector("#viewsManagerStatusActionButton", {
|
||||
visible: true,
|
||||
});
|
||||
await waitAndClick(
|
||||
page,
|
||||
`.thumbnail:has(${getThumbnailSelector(1)}) input`
|
||||
);
|
||||
let handlePagesEdited = await waitForPagesEdited(page);
|
||||
await waitAndClick(page, "#viewsManagerStatusActionButton");
|
||||
await waitAndClick(page, "#viewsManagerStatusActionCopy");
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
// Paste after page 2 so the copy lands at position 3.
|
||||
handlePagesEdited = await waitForPagesEdited(page);
|
||||
await waitAndClick(page, `${getThumbnailSelector(2)}+button`);
|
||||
await awaitPromise(handlePagesEdited);
|
||||
|
||||
// Both the original and the cloned annotation must now be in storage.
|
||||
await waitForStorageEntries(page, 2);
|
||||
|
||||
// Close the reorganize view and navigate to page 3 (the pasted copy)
|
||||
// to trigger rendering of its annotation editor layer.
|
||||
await page.click("#viewsManagerToggleButton");
|
||||
await page.waitForSelector("#viewsManager", { hidden: true });
|
||||
await page.evaluate(() => {
|
||||
window.PDFViewerApplication.pdfViewer.currentPageNumber = 3;
|
||||
});
|
||||
|
||||
// The cloned ink annotation must appear in the DOM of page 3.
|
||||
await page.waitForSelector(`.page[data-page-number="3"] .inkEditor`, {
|
||||
visible: true,
|
||||
});
|
||||
const inkEditors = await page.$$(
|
||||
`.page[data-page-number="3"] .inkEditor`
|
||||
);
|
||||
expect(inkEditors.length).withContext(`In ${browserName}`).toBe(1);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -4203,6 +4203,7 @@ describe("annotation", function () {
|
||||
const changes = new RefSetCache();
|
||||
await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
xref,
|
||||
task,
|
||||
[
|
||||
{
|
||||
@ -4320,6 +4321,7 @@ describe("annotation", function () {
|
||||
const task = new WorkerTask("test FreeText update");
|
||||
await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
xref,
|
||||
task,
|
||||
[
|
||||
{
|
||||
@ -4435,6 +4437,7 @@ describe("annotation", function () {
|
||||
const task = new WorkerTask("test Ink creation");
|
||||
await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
xref,
|
||||
task,
|
||||
[
|
||||
{
|
||||
@ -4532,6 +4535,7 @@ describe("annotation", function () {
|
||||
const task = new WorkerTask("test Ink creation");
|
||||
await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
xref,
|
||||
task,
|
||||
[
|
||||
{
|
||||
@ -4764,6 +4768,7 @@ describe("annotation", function () {
|
||||
const task = new WorkerTask("test Highlight creation");
|
||||
await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
xref,
|
||||
task,
|
||||
[
|
||||
{
|
||||
@ -4857,6 +4862,7 @@ describe("annotation", function () {
|
||||
const task = new WorkerTask("test free Highlight creation");
|
||||
await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
xref,
|
||||
task,
|
||||
[
|
||||
{
|
||||
@ -4988,6 +4994,7 @@ describe("annotation", function () {
|
||||
const task = new WorkerTask("test Highlight update");
|
||||
await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
xref,
|
||||
task,
|
||||
[
|
||||
{
|
||||
@ -5051,6 +5058,7 @@ describe("annotation", function () {
|
||||
const task = new WorkerTask("test Highlight update");
|
||||
await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
xref,
|
||||
task,
|
||||
[
|
||||
{
|
||||
@ -5215,6 +5223,7 @@ describe("annotation", function () {
|
||||
const task = new WorkerTask("test Stamp creation");
|
||||
await AnnotationFactory.saveNewAnnotations(
|
||||
partialEvaluator,
|
||||
xref,
|
||||
task,
|
||||
[
|
||||
{
|
||||
|
||||
@ -6254,6 +6254,79 @@ small scripts as well as for`);
|
||||
await loadingTask.destroy();
|
||||
});
|
||||
|
||||
it("save an ink annotation on a cloned page", async function () {
|
||||
let loadingTask = getDocument(buildGetDocumentParams("empty.pdf"));
|
||||
let pdfDoc = await loadingTask.promise;
|
||||
|
||||
// Simulate what clonePage() puts in annotationStorage when a page is
|
||||
// copied: the original annotation stays on pageIndex 0 and the clone
|
||||
// is placed on pageIndex 1 (the new position of the pasted copy).
|
||||
const inkAnnotation = {
|
||||
annotationType: AnnotationEditorType.INK,
|
||||
rect: [50, 50, 200, 200],
|
||||
rotation: 0,
|
||||
structTreeParentId: null,
|
||||
popupRef: "",
|
||||
color: [0, 0, 255],
|
||||
opacity: 1,
|
||||
thickness: 2,
|
||||
paths: {
|
||||
lines: [
|
||||
new Float32Array([
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
50,
|
||||
200,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
NaN,
|
||||
200,
|
||||
50,
|
||||
]),
|
||||
],
|
||||
points: [[50, 200, 100, 100, 200, 50]],
|
||||
},
|
||||
isCopy: true,
|
||||
};
|
||||
|
||||
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_0", {
|
||||
...inkAnnotation,
|
||||
pageIndex: 0,
|
||||
});
|
||||
pdfDoc.annotationStorage.setValue("pdfjs_internal_editor_1", {
|
||||
...inkAnnotation,
|
||||
pageIndex: 1,
|
||||
});
|
||||
|
||||
// Extract page 0 twice: once at output position 0 (original) and once
|
||||
// at output position 1 (clone), mirroring copy+paste in the UI.
|
||||
const data = await pdfDoc.extractPages([
|
||||
{ document: null, includePages: [0], pageIndices: [0] },
|
||||
{ document: null, includePages: [0], pageIndices: [1] },
|
||||
]);
|
||||
await loadingTask.destroy();
|
||||
|
||||
loadingTask = getDocument(data);
|
||||
pdfDoc = await loadingTask.promise;
|
||||
|
||||
expect(pdfDoc.numPages).toEqual(2);
|
||||
|
||||
// Both pages should carry the ink annotation.
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
const pdfPage = await pdfDoc.getPage(i);
|
||||
const annotations = await pdfPage.getAnnotations();
|
||||
expect(annotations.length).withContext(`Page ${i}`).toEqual(1);
|
||||
expect(annotations[0].annotationType)
|
||||
.withContext(`Page ${i}`)
|
||||
.toEqual(AnnotationType.INK);
|
||||
}
|
||||
|
||||
await loadingTask.destroy();
|
||||
});
|
||||
|
||||
it("fills missing pageIndices with the first free slots", async function () {
|
||||
let loadingTask = getDocument(
|
||||
buildGetDocumentParams("tracemonkey.pdf")
|
||||
|
||||
@ -39,7 +39,6 @@ import { GenericL10n } from "web-null_l10n";
|
||||
* @property {TextLayer} [textLayer]
|
||||
* @property {DrawLayer} [drawLayer]
|
||||
* @property {function} [onAppend]
|
||||
* @property {AnnotationEditorLayer} [clonedFrom]
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -61,8 +60,6 @@ class AnnotationEditorLayerBuilder {
|
||||
|
||||
#uiManager;
|
||||
|
||||
#clonedFrom = null;
|
||||
|
||||
/**
|
||||
* @param {AnnotationEditorLayerBuilderOptions} options
|
||||
*/
|
||||
@ -82,7 +79,6 @@ class AnnotationEditorLayerBuilder {
|
||||
this.#drawLayer = options.drawLayer || null;
|
||||
this.#onAppend = options.onAppend || null;
|
||||
this.#structTreeLayer = options.structTreeLayer || null;
|
||||
this.#clonedFrom = options.clonedFrom || null;
|
||||
}
|
||||
|
||||
updatePageIndex(newPageIndex) {
|
||||
@ -130,11 +126,6 @@ class AnnotationEditorLayerBuilder {
|
||||
drawLayer: this.#drawLayer,
|
||||
});
|
||||
|
||||
this.annotationEditorLayer.setClonedFrom(
|
||||
this.#clonedFrom?.annotationEditorLayer
|
||||
);
|
||||
this.#clonedFrom = null;
|
||||
|
||||
const parameters = {
|
||||
viewport: clonedViewport,
|
||||
div,
|
||||
@ -142,7 +133,7 @@ class AnnotationEditorLayerBuilder {
|
||||
intent,
|
||||
};
|
||||
|
||||
this.annotationEditorLayer.render(parameters);
|
||||
await this.annotationEditorLayer.render(parameters);
|
||||
this.show();
|
||||
}
|
||||
|
||||
|
||||
@ -107,7 +107,6 @@ import { XfaLayerBuilder } from "./xfa_layer_builder.js";
|
||||
* @property {boolean} [enableAutoLinking] - Enable creation of hyperlinks from
|
||||
* text that look like URLs. The default value is `true`.
|
||||
* @property {CommentManager} [commentManager] - The comment manager instance.
|
||||
* @property {PDFPageView} [clonedFrom] - The page view that is cloned
|
||||
* to.
|
||||
*/
|
||||
|
||||
@ -172,8 +171,6 @@ class PDFPageView extends BasePDFPageView {
|
||||
|
||||
#layers = [null, null, null, null];
|
||||
|
||||
#clonedFrom = null;
|
||||
|
||||
/**
|
||||
* @param {PDFPageViewOptions} options
|
||||
*/
|
||||
@ -205,7 +202,6 @@ class PDFPageView extends BasePDFPageView {
|
||||
options.capCanvasAreaFactor ?? AppOptions.get("capCanvasAreaFactor");
|
||||
this.#enableAutoLinking = options.enableAutoLinking !== false;
|
||||
this.#commentManager = options.commentManager || null;
|
||||
this.#clonedFrom = options.clonedFrom || null;
|
||||
|
||||
this.l10n = options.l10n;
|
||||
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
|
||||
@ -301,7 +297,6 @@ class PDFPageView extends BasePDFPageView {
|
||||
enableAutoLinking: this.#enableAutoLinking,
|
||||
commentManager: this.#commentManager,
|
||||
l10n: this.l10n,
|
||||
clonedFrom: this,
|
||||
});
|
||||
clone.setPdfPage(this.pdfPage.clone(id - 1));
|
||||
return clone;
|
||||
@ -355,6 +350,7 @@ class PDFPageView extends BasePDFPageView {
|
||||
if (this.id === newPageNumber) {
|
||||
return;
|
||||
}
|
||||
const oldPageNumber = this.id;
|
||||
this.id = newPageNumber;
|
||||
this.renderingId = `page${newPageNumber}`;
|
||||
if (this.pdfPage) {
|
||||
@ -368,7 +364,11 @@ class PDFPageView extends BasePDFPageView {
|
||||
this._textHighlighter.pageIdx = newPageNumber - 1;
|
||||
// Don't update the page index for the draw layer, since it's just used as
|
||||
// an identifier.
|
||||
this.annotationEditorLayer?.updatePageIndex(newPageNumber - 1);
|
||||
|
||||
this.#layerProperties.annotationEditorUIManager?.updatePageIndex(
|
||||
oldPageNumber - 1,
|
||||
newPageNumber - 1
|
||||
);
|
||||
}
|
||||
|
||||
setPdfPage(pdfPage) {
|
||||
@ -1207,12 +1207,10 @@ class PDFPageView extends BasePDFPageView {
|
||||
annotationLayer: this.annotationLayer?.annotationLayer,
|
||||
textLayer: this.textLayer,
|
||||
drawLayer: this.drawLayer.getDrawLayer(),
|
||||
clonedFrom: this.#clonedFrom?.annotationEditorLayer,
|
||||
onAppend: annotationEditorLayerDiv => {
|
||||
this.#addLayer(annotationEditorLayerDiv, "annotationEditorLayer");
|
||||
},
|
||||
});
|
||||
this.#clonedFrom = null;
|
||||
this.#renderAnnotationEditorLayer();
|
||||
}
|
||||
});
|
||||
|
||||
@ -80,8 +80,6 @@ class TempImageFactory {
|
||||
class PDFThumbnailView extends RenderableView {
|
||||
#renderingState = RenderingStates.INITIAL;
|
||||
|
||||
static foo = 0;
|
||||
|
||||
/**
|
||||
* @param {PDFThumbnailViewOptions} options
|
||||
*/
|
||||
@ -99,7 +97,6 @@ class PDFThumbnailView extends RenderableView {
|
||||
enableSplitMerge = false,
|
||||
}) {
|
||||
super();
|
||||
this.foo = PDFThumbnailView.foo++;
|
||||
this.id = id;
|
||||
this.renderingId = `thumbnail${id}`;
|
||||
this.pageLabel = null;
|
||||
|
||||
@ -1209,6 +1209,7 @@ class PDFViewer {
|
||||
const viewerElement =
|
||||
this._scrollMode === ScrollMode.PAGE ? null : this.viewer;
|
||||
if (viewerElement) {
|
||||
this.#annotationEditorUIManager?.startUpdatePages();
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0, ii = this.#savedPageViews.length; i < ii; i++) {
|
||||
const page = this.#savedPageViews[i];
|
||||
@ -1216,6 +1217,7 @@ class PDFViewer {
|
||||
fragment.append(page.div);
|
||||
}
|
||||
viewerElement.replaceChildren(fragment);
|
||||
this.#annotationEditorUIManager?.endUpdatePages();
|
||||
}
|
||||
this._pages = this.#savedPageViews;
|
||||
this.#savedPageViews = null;
|
||||
@ -1238,6 +1240,9 @@ class PDFViewer {
|
||||
this._currentPageNumber = 0;
|
||||
const prevPages = this._pages;
|
||||
const newPages = (this._pages = []);
|
||||
|
||||
this.#annotationEditorUIManager?.startUpdatePages();
|
||||
|
||||
for (let i = 1, ii = pagesMapper.pagesNumber; i <= ii; i++) {
|
||||
const prevPageNumber = pagesMapper.getPrevPageNumber(i);
|
||||
if (prevPageNumber < 0) {
|
||||
@ -1245,6 +1250,10 @@ class PDFViewer {
|
||||
if (hasBeenCut) {
|
||||
page.updatePageNumber(i);
|
||||
} else {
|
||||
this.#annotationEditorUIManager?.clonePage(
|
||||
-prevPageNumber - 1,
|
||||
i - 1
|
||||
);
|
||||
page = page.clone(i);
|
||||
}
|
||||
newPages.push(page);
|
||||
@ -1255,6 +1264,8 @@ class PDFViewer {
|
||||
page.updatePageNumber(i);
|
||||
}
|
||||
|
||||
this.#annotationEditorUIManager?.endUpdatePages();
|
||||
|
||||
if (type === "paste") {
|
||||
this.#copiedPageViews = null;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user