diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 3198522d7..386629672 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -3301,6 +3301,22 @@ class AnnotationLayer { } else { firstChild.after(canvas); } + + const editableAnnotation = this.#editableAnnotations.get(id); + if (!editableAnnotation) { + continue; + } + if (editableAnnotation._hasNoCanvas) { + // The canvas wasn't available when the annotation was created. + this._annotationEditorUIManager?.setMissingCanvas( + id, + element.id, + canvas + ); + editableAnnotation._hasNoCanvas = false; + } else { + editableAnnotation.canvas = canvas; + } } this.#annotationCanvasMap.clear(); } diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index d646da22f..72ebfa6f8 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -40,6 +40,8 @@ class StampEditor extends AnnotationEditor { #canvas = null; + #missingCanvas = false; + #resizeTimeoutId = null; #isSvg = false; @@ -352,7 +354,8 @@ class StampEditor extends AnnotationEditor { this.#bitmap || this.#bitmapUrl || this.#bitmapFile || - this.#bitmapId + this.#bitmapId || + this.#missingCanvas ); } @@ -379,10 +382,12 @@ class StampEditor extends AnnotationEditor { this.addAltTextButton(); - if (this.#bitmap) { - this.#createCanvas(); - } else { - this.#getBitmap(); + if (!this.#missingCanvas) { + if (this.#bitmap) { + this.#createCanvas(); + } else { + this.#getBitmap(); + } } if (this.width && !this.annotationElementId) { @@ -401,6 +406,22 @@ class StampEditor extends AnnotationEditor { return this.div; } + setCanvas(annotationElementId, canvas) { + const { id: bitmapId, bitmap } = this._uiManager.imageManager.getFromCanvas( + annotationElementId, + canvas + ); + canvas.remove(); + if (bitmapId && this._uiManager.imageManager.isValidId(bitmapId)) { + this.#bitmapId = bitmapId; + if (bitmap) { + this.#bitmap = bitmap; + } + this.#missingCanvas = false; + this.#createCanvas(); + } + } + /** @inheritdoc */ _onResized() { // We used a CSS-zoom during the resizing, but now it's resized we can @@ -752,6 +773,7 @@ class StampEditor extends AnnotationEditor { /** @inheritdoc */ static async deserialize(data, parent, uiManager) { let initialData = null; + let missingCanvas = false; if (data instanceof StampAnnotationElement) { const { data: { rect, rotation, id, structParent, popupRef }, @@ -759,13 +781,20 @@ class StampEditor extends AnnotationEditor { parent: { page: { pageNumber }, }, + canvas, } = data; - const canvas = container.querySelector("canvas"); - const imageData = uiManager.imageManager.getFromCanvas( - container.id, - canvas - ); - canvas.remove(); + let bitmapId, bitmap; + if (canvas) { + delete data.canvas; + ({ id: bitmapId, bitmap } = uiManager.imageManager.getFromCanvas( + container.id, + canvas + )); + canvas.remove(); + } else { + missingCanvas = true; + data._hasNoCanvas = true; + } // When switching to edit mode, we wait for the structure tree to be // ready (see pdf_viewer.js), so it's fine to use getAriaAttributesSync. @@ -776,8 +805,8 @@ class StampEditor extends AnnotationEditor { initialData = data = { annotationType: AnnotationEditorType.STAMP, - bitmapId: imageData.id, - bitmap: imageData.bitmap, + bitmapId, + bitmap, pageIndex: pageNumber - 1, rect: rect.slice(0), rotation, @@ -795,7 +824,10 @@ class StampEditor extends AnnotationEditor { const editor = await super.deserialize(data, parent, uiManager); const { rect, bitmap, bitmapUrl, bitmapId, isSvg, accessibilityData } = data; - if (bitmapId && uiManager.imageManager.isValidId(bitmapId)) { + if (missingCanvas) { + uiManager.addMissingCanvas(data.id, editor); + editor.#missingCanvas = true; + } else if (bitmapId && uiManager.imageManager.isValidId(bitmapId)) { editor.#bitmapId = bitmapId; if (bitmap) { editor.#bitmap = bitmap; diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index d005a5d2b..af2f49cd3 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -654,6 +654,8 @@ class AnnotationEditorUIManager { #mainHighlightColorPicker = null; + #missingCanvases = null; + #mlManager = null; #mode = AnnotationEditorType.NONE; @@ -898,6 +900,7 @@ class AnnotationEditorUIManager { this.#allLayers.clear(); this.#allEditors.clear(); this.#editorsToRescale.clear(); + this.#missingCanvases?.clear(); this.#activeEditor = null; this.#selectedEditors.clear(); this.#commandManager.destroy(); @@ -1711,6 +1714,10 @@ class AnnotationEditorUIManager { this.#updateModeCapability.resolve(); } + isInEditingMode() { + return this.#mode !== AnnotationEditorType.NONE; + } + addNewEditorFromKeyboard() { if (this.currentLayer.canCreateNewEmptyEditor()) { this.currentLayer.addNewEditor(); @@ -1887,6 +1894,9 @@ class AnnotationEditorUIManager { }, 0); } this.#allEditors.delete(editor.id); + if (editor.annotationElementId) { + this.#missingCanvases?.delete(editor.annotationElementId); + } this.unselect(editor); if ( !editor.annotationElementId || @@ -2514,6 +2524,19 @@ class AnnotationEditorUIManager { } editor.renderAnnotationElement(annotation); } + + setMissingCanvas(annotationId, annotationElementId, canvas) { + const editor = this.#missingCanvases?.get(annotationId); + if (!editor) { + return; + } + editor.setCanvas(annotationElementId, canvas); + this.#missingCanvases.delete(annotationId); + } + + addMissingCanvas(annotationId, editor) { + (this.#missingCanvases ||= new Map()).set(annotationId, editor); + } } export { diff --git a/test/integration/stamp_editor_spec.mjs b/test/integration/stamp_editor_spec.mjs index acbc13a62..afbc1e6be 100644 --- a/test/integration/stamp_editor_spec.mjs +++ b/test/integration/stamp_editor_spec.mjs @@ -1745,4 +1745,73 @@ describe("Stamp Editor", () => { ); }); }); + + describe("Switch to edit mode a pdf with an existing stamp annotation on an invisible and rendered page", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue19239.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must move on the second page", async () => { + await Promise.all( + pages.map(async ([, page]) => { + const pageOneSelector = `.page[data-page-number = "1"]`; + const pageTwoSelector = `.page[data-page-number = "2"]`; + await scrollIntoView(page, pageTwoSelector); + await page.waitForSelector(pageOneSelector, { + visible: false, + }); + + await switchToStamp(page); + await scrollIntoView(page, pageOneSelector); + await page.waitForSelector( + `${pageOneSelector} .annotationEditorLayer canvas`, + { visible: true } + ); + }) + ); + }); + }); + + describe("Switch to edit mode a pdf with an existing stamp annotation on an invisible and unrendered page", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue19239.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must move on the last page", async () => { + await Promise.all( + pages.map(async ([, page]) => { + const twoToFourteen = Array.from(new Array(13).keys(), n => n + 2); + for (const pageNumber of twoToFourteen) { + const pageSelector = `.page[data-page-number = "${pageNumber}"]`; + await scrollIntoView(page, pageSelector); + } + + await switchToStamp(page); + + const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n); + for (const pageNumber of thirteenToOne) { + const pageSelector = `.page[data-page-number = "${pageNumber}"]`; + await scrollIntoView(page, pageSelector); + } + + await page.waitForSelector( + `.page[data-page-number = "1"] .annotationEditorLayer canvas`, + { visible: true } + ); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 7b9182629..3cfd7a0e0 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -693,3 +693,4 @@ !issue19182.pdf !issue18911.pdf !issue19207.pdf +!issue19239.pdf diff --git a/test/pdfs/issue19239.pdf b/test/pdfs/issue19239.pdf new file mode 100755 index 000000000..4e1b5d92a Binary files /dev/null and b/test/pdfs/issue19239.pdf differ diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 26b3257f9..963dd644b 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -588,10 +588,14 @@ class PDFPageView { } toggleEditingMode(isEditing) { + // The page can be invisible, consequently there's no annotation layer and + // we can't know if there are editable annotations. + // So to avoid any issue when the page is rendered the #isEditing flag must + // be set. + this.#isEditing = isEditing; if (!this.hasEditableAnnotations()) { return; } - this.#isEditing = isEditing; this.reset({ keepAnnotationLayer: true, keepAnnotationEditorLayer: true,