diff --git a/src/core/annotation.js b/src/core/annotation.js index 1402f32ead63c1..9ff973f74b330c 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -693,6 +693,7 @@ class Annotation { hasOwnCanvas: false, noRotate: !!(this.flags & AnnotationFlag.NOROTATE), noHTML: isLocked && isContentLocked, + isEditable: false, }; if (params.collectFields) { @@ -789,6 +790,10 @@ class Annotation { return this.printable; } + mustBeViewedWhenEditing() { + return !this.data.isEditable; + } + /** * @type {boolean} */ @@ -3802,7 +3807,8 @@ class FreeTextAnnotation extends MarkupAnnotation { // It uses its own canvas in order to be hidden if edited. // But if it has the noHTML flag, it means that we don't want to be able // to modify it so we can just draw it on the main canvas. - this.data.hasOwnCanvas = !this.data.noHTML; + this.data.hasOwnCanvas = this.data.noRotate; + this.data.isEditable = !this.data.noHTML; // We want to be able to add mouse listeners to the annotation. this.data.noHTML = false; diff --git a/src/core/document.js b/src/core/document.js index 505cd9a75834b0..652567288a0056 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -531,6 +531,8 @@ class Page { }); }); + const modifiedIds = this.pdfManager.modifiedIds; + // Fetch the page's annotations and add their operator lists to the // page's operator list to render them. return Promise.all([ @@ -570,7 +572,10 @@ class Page { const renderForms = !!(intent & RenderingIntentFlag.ANNOTATIONS_FORMS), intentAny = !!(intent & RenderingIntentFlag.ANY), intentDisplay = !!(intent & RenderingIntentFlag.DISPLAY), - intentPrint = !!(intent & RenderingIntentFlag.PRINT); + intentPrint = !!(intent & RenderingIntentFlag.PRINT), + intentDisplayEdit = !!( + intent & RenderingIntentFlag.ANNOTATIONS_DISPLAY_EDIT + ); // Collect the operator list promises for the annotations. Each promise // is resolved with the complete operator list for a single annotation. @@ -579,7 +584,11 @@ class Page { if ( intentAny || (intentDisplay && - annotation.mustBeViewed(annotationStorage, renderForms)) || + ((intentDisplayEdit && + annotation.mustBeViewedWhenEditing(annotationStorage)) || + (!intentDisplayEdit && + annotation.mustBeViewed(annotationStorage, renderForms) && + !modifiedIds?.has(annotation.data.id)))) || (intentPrint && annotation.mustBePrinted(annotationStorage)) ) { opListPromises.push( @@ -910,6 +919,7 @@ class PDFDocument { this.xref = new XRef(stream, pdfManager); this._pagePromises = new Map(); this._version = null; + this.modifiedIds = null; const idCounters = { font: 0, diff --git a/src/core/pdf_manager.js b/src/core/pdf_manager.js index aee66820ab15de..b676f0270befb4 100644 --- a/src/core/pdf_manager.js +++ b/src/core/pdf_manager.js @@ -44,6 +44,7 @@ class BasePdfManager { this._docId = args.docId; this._password = args.password; this.enableXfa = args.enableXfa; + this.modifiedIds = null; // Check `OffscreenCanvas` support once, rather than repeatedly throughout // the worker-thread code. @@ -127,6 +128,10 @@ class BasePdfManager { terminate(reason) { unreachable("Abstract method `terminate` called"); } + + endEditingSession(ids) { + this.modifiedIds = ids ? new Set(ids) : null; + } } class LocalPdfManager extends BasePdfManager { diff --git a/src/core/worker.js b/src/core/worker.js index 804936229bcc93..87f10aa8ff39aa 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -873,6 +873,10 @@ class WorkerMessageHandler { docParams = null; // we don't need docParams anymore -- saving memory. }); + handler.on("EndEditingSession", function (ids) { + return pdfManager.endEditingSession(ids); + }); + if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { handler.on("GetXFADatasets", function (data) { return pdfManager.ensureDoc("xfaDatasets"); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index d55386b78fca74..77017cf6fe4a58 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -198,6 +198,10 @@ class AnnotationElement { return !!(titleObj?.str || contentsObj?.str || richText?.str); } + get _isEditable() { + return this.data.isEditable; + } + get hasPopupData() { return AnnotationElement._hasPopupData(this.data); } @@ -728,10 +732,6 @@ class AnnotationElement { } } - get _isEditable() { - return false; - } - _editOnDoubleClick() { if (!this._isEditable) { return; @@ -2524,10 +2524,6 @@ class FreeTextAnnotationElement extends AnnotationElement { return this.container; } - - get _isEditable() { - return this.data.hasOwnCanvas; - } } class LineAnnotationElement extends AnnotationElement { @@ -3094,6 +3090,10 @@ class AnnotationLayer { } } + hasEditableAnnotations() { + return this.#editableAnnotations.size > 0; + } + #appendElement(element, id) { const contentElement = element.firstChild || element; contentElement.id = `${AnnotationPrefix}${id}`; @@ -3175,7 +3175,7 @@ class AnnotationLayer { } this.#appendElement(rendered, data.id); - if (element.annotationEditorType > 0) { + if (element._isEditable) { this.#editableAnnotations.set(element.data.id, element); this._annotationEditorUIManager?.renderAnnotationElement(element); } diff --git a/src/display/annotation_storage.js b/src/display/annotation_storage.js index 8154453c3b48b7..d807ea000ee0a3 100644 --- a/src/display/annotation_storage.js +++ b/src/display/annotation_storage.js @@ -248,6 +248,21 @@ class AnnotationStorage { } return stats; } + + get modifiedIds() { + let ids = null; + for (const value of this.#storage.values()) { + if ( + !(value instanceof AnnotationEditor) || + !value.annotationElementId || + !value.serialize() + ) { + continue; + } + (ids ||= []).push(value.annotationElementId); + } + return ids; + } } /** diff --git a/src/display/api.js b/src/display/api.js index fd7ba98cc9f813..071587c3ffa5e5 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -21,6 +21,7 @@ import { AbortException, AnnotationMode, assert, + EditingStatus, getVerbosityLevel, info, InvalidPDFException, @@ -1103,6 +1104,13 @@ class PDFDocumentProxy { getCalculationOrderIds() { return this._transport.getCalculationOrderIds(); } + + /** + * Ends the editing session for the current document. + */ + endEditingSession() { + return this._transport.endEditingSession(); + } } /** @@ -1229,6 +1237,7 @@ class PDFDocumentProxy { * @property {Map} [annotationCanvasMap] - Map some * annotation ids with canvases used to render them. * @property {PrintAnnotationStorage} [printAnnotationStorage] + * @property {number} [editingStatus] - Controls if the page is in editing mode. */ /** @@ -1422,13 +1431,16 @@ class PDFPageProxy { annotationCanvasMap = null, pageColors = null, printAnnotationStorage = null, + editingStatus = EditingStatus.NONE, }) { this._stats?.time("Overall"); const intentArgs = this._transport.getRenderingIntent( intent, annotationMode, - printAnnotationStorage + printAnnotationStorage, + /* isOpList = */ false, + editingStatus ); const { renderingIntent, cacheKey } = intentArgs; // If there was a pending destroy, cancel it so no cleanup happens during @@ -1440,7 +1452,8 @@ class PDFPageProxy { optionalContentConfigPromise ||= this._transport.getOptionalContentConfig(renderingIntent); - let intentState = this._intentStates.get(cacheKey); + let intentState = + editingStatus !== EditingStatus.END && this._intentStates.get(cacheKey); if (!intentState) { intentState = Object.create(null); this._intentStates.set(cacheKey, intentState); @@ -2427,7 +2440,8 @@ class WorkerTransport { intent, annotationMode = AnnotationMode.ENABLE, printAnnotationStorage = null, - isOpList = false + isOpList = false, + editingStatus = EditingStatus.NONE ) { let renderingIntent = RenderingIntentFlag.DISPLAY; // Default value. let annotationStorageSerializable = SerializableEmpty; @@ -2473,6 +2487,10 @@ class WorkerTransport { renderingIntent += RenderingIntentFlag.OPLIST; } + if (editingStatus === EditingStatus.START) { + renderingIntent += RenderingIntentFlag.ANNOTATIONS_DISPLAY_EDIT; + } + return { renderingIntent, cacheKey: `${renderingIntent}_${annotationStorageSerializable.hash}`, @@ -3098,6 +3116,13 @@ class WorkerTransport { const refStr = ref.gen === 0 ? `${ref.num}R` : `${ref.num}R${ref.gen}`; return this.#pageRefCache.get(refStr) ?? null; } + + endEditingSession() { + this.messageHandler.sendWithPromise( + "EndEditingSession", + this.annotationStorage.modifiedIds + ); + } } const INITIAL_DATA = Symbol("INITIAL_DATA"); diff --git a/src/shared/util.js b/src/shared/util.js index 68d12cd4cd21c1..f68db40c151b91 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -56,6 +56,7 @@ const RenderingIntentFlag = { ANNOTATIONS_FORMS: 0x10, ANNOTATIONS_STORAGE: 0x20, ANNOTATIONS_DISABLE: 0x40, + ANNOTATIONS_DISPLAY_EDIT: 0x80, OPLIST: 0x100, }; @@ -66,6 +67,12 @@ const AnnotationMode = { ENABLE_STORAGE: 3, }; +const EditingStatus = { + NONE: 0, + START: 1, + END: 2, +}; + const AnnotationEditorPrefix = "pdfjs_internal_editor_"; const AnnotationEditorType = { @@ -1105,6 +1112,7 @@ export { CMapCompressionType, createValidAbsoluteUrl, DocumentActionEventType, + EditingStatus, FeatureTest, FONT_IDENTITY_MATRIX, FontRenderOps, diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index d7afe96ab52c4c..84ba3f42b8db8c 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -44,6 +44,7 @@ import { scrollIntoView, switchToEditor, waitForAnnotationEditorLayer, + waitForAnnotationModeChanged, waitForEvent, waitForSelectedEditor, waitForSerialized, @@ -995,6 +996,20 @@ describe("FreeText Editor", () => { pages.map(async ([browserName, page]) => { await switchToFreeText(page); + // The page has been re-rendered but with no freetext annotations. + const isWhite = await page.evaluate(() => { + const canvas = document.querySelector(".canvasWrapper canvas"); + const ctx = canvas.getContext("2d"); + const { data } = ctx.getImageData( + 0, + 0, + canvas.width, + canvas.height + ); + return data.every(x => x === 0xff); + }); + expect(isWhite).withContext(`In ${browserName}`).toBeTrue(); + let editorIds = await getEditors(page, "freeText"); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6); @@ -1049,11 +1064,19 @@ describe("FreeText Editor", () => { // canvas. editorIds = await getEditors(page, "freeText"); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1); - const hidden = await page.$eval( - "[data-annotation-id='26R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeTrue(); + + const isEditorWhite = await page.evaluate(rect => { + const canvas = document.querySelector(".canvasWrapper canvas"); + const ctx = canvas.getContext("2d"); + const { data } = ctx.getImageData( + rect.x, + rect.y, + rect.width, + rect.height + ); + return data.every(x => x === 0xff); + }, editorRect); + expect(isEditorWhite).withContext(`In ${browserName}`).toBeTrue(); // Check we've now a div containing the text. const newDivText = await page.$eval( @@ -1295,10 +1318,12 @@ describe("FreeText Editor", () => { await closePages(pages); }); - it("must move an annotation", async () => { + it("must edit an annotation", async () => { await Promise.all( pages.map(async ([browserName, page]) => { + const modeChangedHandle = await waitForAnnotationModeChanged(page); await page.click("[data-annotation-id='26R']", { count: 2 }); + await awaitPromise(modeChangedHandle); await page.waitForSelector(`${getEditorSelector(0)}-editor`); const [focusedId, editable] = await page.evaluate(() => { @@ -1354,6 +1379,7 @@ describe("FreeText Editor", () => { // TODO: remove this when we switch to BiDi. await hover(page, "[data-annotation-id='23R']"); + // Wait for the popup to be displayed. await page.waitForFunction( () => @@ -1595,12 +1621,6 @@ describe("FreeText Editor", () => { it("must open an existing annotation and check that the position are good", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await switchToFreeText(page); - - await page.evaluate(() => { - document.getElementById("editorFreeTextParamsToolbar").remove(); - }); - const toBinary = buf => { for (let i = 0; i < buf.length; i += 4) { const gray = @@ -1653,8 +1673,11 @@ describe("FreeText Editor", () => { return null; }; - for (const n of [0, 1, 2, 3, 4]) { - const rect = await getRect(page, getEditorSelector(n)); + const firstPixelsAnnotations = new Map(); + + for (const n of [26, 32, 42, 57, 35, 1]) { + const id = `${n}R`; + const rect = await getRect(page, `[data-annotation-id="${id}"]`); const editorPng = await page.screenshot({ clip: rect, type: "png", @@ -1665,33 +1688,33 @@ describe("FreeText Editor", () => { editorImage.width, editorImage.height ); + firstPixelsAnnotations.set(id, { editorFirstPix, rect }); + } + await switchToFreeText(page); + + await page.evaluate(() => { + document.getElementById("editorFreeTextParamsToolbar").remove(); + }); + + for (const n of [0, 1, 2, 3, 4]) { const annotationId = await page.evaluate(N => { const editor = document.getElementById( `pdfjs_internal_editor_${N}` ); - const annId = editor.getAttribute("annotation-id"); - const annotation = document.querySelector( - `[data-annotation-id="${annId}"]` - ); - editor.hidden = true; - annotation.hidden = false; - return annId; + return editor.getAttribute("annotation-id"); }, n); - await page.waitForSelector(`${getEditorSelector(n)}[hidden]`); - await page.waitForSelector( - `[data-annotation-id="${annotationId}"]:not([hidden])` - ); - - const annotationPng = await page.screenshot({ + const { editorFirstPix: annotationFirstPix, rect } = + firstPixelsAnnotations.get(annotationId); + const editorPng = await page.screenshot({ clip: rect, type: "png", }); - const annotationImage = PNG.sync.read(annotationPng); - const annotationFirstPix = getFirstPixel( - annotationImage.data, - annotationImage.width, - annotationImage.height + const editorImage = PNG.sync.read(editorPng); + const editorFirstPix = getFirstPixel( + editorImage.data, + editorImage.width, + editorImage.height ); expect( @@ -1726,12 +1749,6 @@ describe("FreeText Editor", () => { it("must open an existing rotated annotation and check that the position are good", async () => { await Promise.all( pages.map(async ([browserName, page]) => { - await switchToFreeText(page); - - await page.evaluate(() => { - document.getElementById("editorFreeTextParamsToolbar").remove(); - }); - const toBinary = buf => { for (let i = 0; i < buf.length; i += 4) { const gray = @@ -1813,13 +1830,15 @@ describe("FreeText Editor", () => { return null; }; + const firstPixelsAnnotations = new Map(); for (const [n, start] of [ - [0, "BL"], - [1, "BR"], - [2, "TR"], - [3, "TL"], + [17, "BL"], + [18, "BR"], + [19, "TR"], + [20, "TL"], ]) { - const rect = await getRect(page, getEditorSelector(n)); + const id = `${n}R`; + const rect = await getRect(page, `[data-annotation-id="${id}"]`); const editorPng = await page.screenshot({ clip: rect, type: "png", @@ -1831,33 +1850,38 @@ describe("FreeText Editor", () => { editorImage.height, start ); + firstPixelsAnnotations.set(id, { editorFirstPix, rect }); + } + + await switchToFreeText(page); + await page.evaluate(() => { + document.getElementById("editorFreeTextParamsToolbar").remove(); + }); + + for (const [n, start] of [ + [0, "BL"], + [1, "BR"], + [2, "TR"], + [3, "TL"], + ]) { const annotationId = await page.evaluate(N => { const editor = document.getElementById( `pdfjs_internal_editor_${N}` ); - const annId = editor.getAttribute("annotation-id"); - const annotation = document.querySelector( - `[data-annotation-id="${annId}"]` - ); - editor.hidden = true; - annotation.hidden = false; - return annId; + return editor.getAttribute("annotation-id"); }, n); - await page.waitForSelector(`${getEditorSelector(n)}[hidden]`); - await page.waitForSelector( - `[data-annotation-id="${annotationId}"]:not([hidden])` - ); - - const annotationPng = await page.screenshot({ + const { editorFirstPix: annotationFirstPix, rect } = + firstPixelsAnnotations.get(annotationId); + const editorPng = await page.screenshot({ clip: rect, type: "png", }); - const annotationImage = PNG.sync.read(annotationPng); - const annotationFirstPix = getFirstPixel( - annotationImage.data, - annotationImage.width, - annotationImage.height, + const editorImage = PNG.sync.read(editorPng); + const editorFirstPix = getFirstPixel( + editorImage.data, + editorImage.width, + editorImage.height, start ); @@ -3523,7 +3547,7 @@ describe("FreeText Editor", () => { }); describe("Update a freetext and scroll", () => { - let pages; + let pages; // CALIXTE beforeAll(async () => { pages = await loadAndWait( @@ -3579,13 +3603,6 @@ describe("FreeText Editor", () => { ); } - await page.waitForSelector("[data-annotation-id='998R'] canvas"); - let hidden = await page.$eval( - "[data-annotation-id='998R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeTrue(); - // Check we've now a div containing the text. await page.waitForSelector( "[data-annotation-id='998R'] div.annotationContent" @@ -3598,6 +3615,24 @@ describe("FreeText Editor", () => { .withContext(`In ${browserName}`) .toEqual("Hello World and edited in Firefox"); + // Check that the canvas has nothing drawn at the annotation place. + await page.$eval( + "[data-annotation-id='998R']", + el => (el.hidden = true) + ); + let editorPng = await page.screenshot({ + clip: editorRect, + type: "png", + }); + await page.$eval( + "[data-annotation-id='998R']", + el => (el.hidden = false) + ); + let editorImage = PNG.sync.read(editorPng); + expect(editorImage.data.every(x => x === 0xff)) + .withContext(`In ${browserName}`) + .toBeTrue(); + const oneToThirteen = Array.from(new Array(13).keys(), n => n + 2); for (const pageNumber of oneToThirteen) { await scrollIntoView( @@ -3614,6 +3649,19 @@ describe("FreeText Editor", () => { await switchToFreeText(page, /* disable = */ true); const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n); + const handlePromise = await createPromise(page, resolve => { + const callback = e => { + if (e.source.id === 1) { + window.PDFViewerApplication.eventBus.off( + "pagerendered", + callback + ); + resolve(); + } + }; + window.PDFViewerApplication.eventBus.on("pagerendered", callback); + }); + for (const pageNumber of thirteenToOne) { await scrollIntoView( page, @@ -3621,12 +3669,16 @@ describe("FreeText Editor", () => { ); } - await page.waitForSelector("[data-annotation-id='998R'] canvas"); - hidden = await page.$eval( - "[data-annotation-id='998R'] canvas", - el => getComputedStyle(el).display === "none" - ); - expect(hidden).withContext(`In ${browserName}`).toBeFalse(); + await awaitPromise(handlePromise); + + editorPng = await page.screenshot({ + clip: editorRect, + type: "png", + }); + editorImage = PNG.sync.read(editorPng); + expect(editorImage.data.every(x => x === 0xff)) + .withContext(`In ${browserName}`) + .toBeFalse(); }) ); }); diff --git a/test/integration/stamp_editor_spec.mjs b/test/integration/stamp_editor_spec.mjs index 82979e6f85c8e2..cfc1c68ad01bfd 100644 --- a/test/integration/stamp_editor_spec.mjs +++ b/test/integration/stamp_editor_spec.mjs @@ -565,14 +565,14 @@ describe("Stamp Editor", () => { for (let i = 0; i < pages1.length; i++) { const [, page1] = pages1[i]; page1.bringToFront(); - await page1.click("#editorStamp"); + await switchToStamp(page1); await copyImage(page1, "../images/firefox_logo.png", 0); await kbCopy(page1); const [, page2] = pages2[i]; page2.bringToFront(); - await page2.click("#editorStamp"); + await switchToStamp(page2); await kbPaste(page2); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 2113c4ed224b19..f352c4047fd5d3 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -401,6 +401,21 @@ function waitForAnnotationEditorLayer(page) { }); } +function waitForAnnotationModeChanged(page) { + return createPromise(page, resolve => { + window.PDFViewerApplication.eventBus.on( + "annotationeditormodechanged", + resolve + ); + }); +} + +function waitForPageRendered(page) { + return createPromise(page, resolve => { + window.PDFViewerApplication.eventBus.on("pagerendered", resolve); + }); +} + async function scrollIntoView(page, selector) { const handle = await page.evaluateHandle( sel => [ @@ -641,8 +656,10 @@ export { serializeBitmapDimensions, switchToEditor, waitForAnnotationEditorLayer, + waitForAnnotationModeChanged, waitForEntryInStorage, waitForEvent, + waitForPageRendered, waitForSandboxTrip, waitForSelectedEditor, waitForSerialized, diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index 481a3e1e42c8a7..2b56d506cd86fd 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -182,6 +182,10 @@ class AnnotationLayerBuilder { this.div.hidden = true; } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + #updatePresentationModeState(state) { if (!this.div) { return; diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index b40f2dcc507c54..c65e7632833e5e 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -43,6 +43,7 @@ import { AnnotationEditorLayerBuilder } from "./annotation_editor_layer_builder. import { AnnotationLayerBuilder } from "./annotation_layer_builder.js"; import { AppOptions } from "./app_options.js"; import { DrawLayerBuilder } from "./draw_layer_builder.js"; +import { EditingStatus } from "../src/shared/util.js"; import { GenericL10n } from "web-null_l10n"; import { SimpleLinkService } from "./pdf_link_service.js"; import { StructTreeLayerBuilder } from "./struct_tree_layer_builder.js"; @@ -113,6 +114,8 @@ const LAYERS_ORDER = new Map([ class PDFPageView { #annotationMode = AnnotationMode.ENABLE_FORMS; + #editingStatus = EditingStatus.NONE; + #hasRestrictedScaling = false; #layerProperties = null; @@ -369,6 +372,10 @@ class PDFPageView { }); } + hasEditableAnnotations() { + return !!this.annotationLayer?.hasEditableAnnotations(); + } + async #renderAnnotationLayer() { let error = null; try { @@ -577,6 +584,23 @@ class PDFPageView { } } + /** + * @param {boolean} start true to switch to editing mode, false otherwise. + */ + switchToEditingMode(start) { + if (!this.hasEditableAnnotations()) { + return; + } + this.#editingStatus = start ? EditingStatus.START : EditingStatus.END; + this.reset({ + keepZoomLayer: true, + keepAnnotationLayer: true, + keepAnnotationEditorLayer: true, + keepXfaLayer: true, + keepTextLayer: true, + }); + } + /** * @typedef {Object} PDFPageViewUpdateParameters * @property {number} [scale] The new scale, if specified. @@ -1029,8 +1053,12 @@ class PDFPageView { optionalContentConfigPromise: this._optionalContentConfigPromise, annotationCanvasMap: this._annotationCanvasMap, pageColors, + editingStatus: this.#editingStatus, }; const renderTask = (this.renderTask = pdfPage.render(renderContext)); + if (this.#editingStatus === EditingStatus.END) { + this.#editingStatus = EditingStatus.NONE; + } renderTask.onContinue = renderContinueCallback; const resultPromise = renderTask.promise.then( diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index d52d1e25a876df..cd29d701ae45c2 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -1590,6 +1590,43 @@ class PDFViewer { }; } + switchToEditAnnotationMode() { + const visible = this._getVisiblePages(); + const pagesToRefresh = []; + const { first, ids, last, views } = visible; + let keepFirst = false; + let keepLast = false; + for (const page of views) { + const { view } = page; + if (!view.hasEditableAnnotations()) { + ids.delete(view.id); + continue; + } + pagesToRefresh.push(page); + if (page === first) { + keepFirst = true; + } + if (page === last) { + keepLast = true; + } + } + + if (pagesToRefresh.length === 0) { + return null; + } + if (!keepFirst) { + visible.first = pagesToRefresh[0]; + } + if (!keepLast) { + visible.last = pagesToRefresh.at(-1); + } + visible.views = pagesToRefresh; + + this.renderingQueue.renderHighestPriority(visible); + + return pagesToRefresh; + } + update() { const visible = this._getVisiblePages(); const visiblePages = visible.views, @@ -2236,13 +2273,49 @@ class PDFViewer { if (!this.pdfDocument) { return; } - this.#annotationEditorMode = mode; - this.eventBus.dispatch("annotationeditormodechanged", { - source: this, - mode, - }); - this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); + if ( + mode === AnnotationEditorType.NONE || + this.#annotationEditorMode === AnnotationEditorType.NONE + ) { + const isEditing = mode !== AnnotationEditorType.NONE; + if (!isEditing) { + this.pdfDocument.endEditingSession(); + } + for (const pageView of this._pages) { + pageView.switchToEditingMode(isEditing); + } + const pageToRefresh = this.switchToEditAnnotationMode(); + const { eventBus } = this; + const updater = () => { + this.#annotationEditorMode = mode; + eventBus.dispatch("annotationeditormodechanged", { + source: this, + mode, + }); + this.#annotationEditorUIManager.updateMode( + mode, + editId, + isFromKeyboard + ); + }; + if (isEditing && editId && pageToRefresh) { + // We're editing an existing annotation so we must switch to editing + // mode when the rendering is done. + const { signal } = this.#eventAbortController; + const ids = new Set(pageToRefresh.map(page => page.id)); + const onPageRendered = ({ source: { id } }) => { + ids.delete(id); + if (ids.size === 0) { + eventBus._off("pagerendered", onPageRendered); + setTimeout(updater, 0); + } + }; + eventBus._on("pagerendered", onPageRendered, { signal }); + return; + } + updater(); + } } // eslint-disable-next-line accessor-pairs