diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 0950b806e..5abf8c8dc 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -17,6 +17,7 @@ import { BaseException, DrawOPS, FeatureTest, + makeArr, MathClamp, shadow, stripPath, @@ -1360,9 +1361,7 @@ class PagesMapper { * Gets the current page mapping suitable for saving. * @returns {Object} An object containing the page indices. */ - getPageMappingForSaving() { - const idToPageNumber = this.#idToPageNumber; - + getPageMappingForSaving(idToPageNumber = this.#idToPageNumber) { // idToPageNumber maps used 1-based IDs to 1-based page numbers. // For example if the final pdf contains page 3 twice and they are moved at // page 1 and 4, then it contains: @@ -1413,6 +1412,19 @@ class PagesMapper { return extractParams; } + extractPages(extractedPageNumbers) { + extractedPageNumbers = Array.from(extractedPageNumbers).sort( + (a, b) => a - b + ); + const usedIds = new Map(); + for (let i = 0, ii = extractedPageNumbers.length; i < ii; i++) { + const id = this.getPageId(extractedPageNumbers[i]); + const usedPageNumbers = usedIds.getOrInsertComputed(id, makeArr); + usedPageNumbers.push(i + 1); + } + return this.getPageMappingForSaving(usedIds); + } + /** * Gets the previous page number for a given page number. * @param {number} pageNumber diff --git a/test/integration/reorganize_pages_spec.mjs b/test/integration/reorganize_pages_spec.mjs index 8d7572e77..6c664cd74 100644 --- a/test/integration/reorganize_pages_spec.mjs +++ b/test/integration/reorganize_pages_spec.mjs @@ -614,24 +614,14 @@ describe("Reorganize Pages View", () => { 10 ); - const handleSaveAs = await createPromise(page, resolve => { - window.PDFViewerApplication.eventBus.on( - "savepageseditedpdf", - ({ data }) => { - resolve(Array.from(data[0].pageIndices)); - }, - { - once: true, - } - ); + const handleSave = await createPromise(page, resolve => { + window.PDFViewerApplication.onSavePages = async ({ data }) => { + resolve(Array.from(data[0].pageIndices)); + }; }); - await page.click("#viewsManagerStatusActionButton"); - await page.waitForSelector("#viewsManagerStatusActionSaveAs", { - visible: true, - }); - await page.click("#viewsManagerStatusActionSaveAs"); - const pageIndices = await awaitPromise(handleSaveAs); + await waitAndClick(page, "#downloadButton"); + const pageIndices = await awaitPromise(handleSave); expect(pageIndices) .withContext(`In ${browserName}`) .toEqual([ @@ -1041,4 +1031,59 @@ describe("Reorganize Pages View", () => { ); }); }); + + describe("Extract some pages from a pdf", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "page_with_number.pdf", + "#viewsManagerToggleButton", + "page-fit", + null, + { enableSplitMerge: true } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("should check that the pages are correctly extracted", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await waitForThumbnailVisible(page, 1); + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(1)}) input` + ); + await waitAndClick( + page, + `.thumbnail:has(${getThumbnailSelector(3)}) input` + ); + + const handleSaveAs = await createPromise(page, resolve => { + window.PDFViewerApplication.eventBus.on( + "saveextractedpages", + ({ data }) => { + resolve(data); + }, + { + once: true, + } + ); + }); + + await page.click("#viewsManagerStatusActionButton"); + await waitAndClick(page, "#viewsManagerStatusActionSaveAs"); + const pagesData = await awaitPromise(handleSaveAs); + expect(pagesData) + .withContext(`In ${browserName}`) + .toEqual([ + { document: null, pageIndices: [0, 1], includePages: [0, 2] }, + ]); + }) + ); + }); + }); }); diff --git a/web/app.js b/web/app.js index a22ac8313..8d77e8fb0 100644 --- a/web/app.js +++ b/web/app.js @@ -1072,9 +1072,9 @@ const PDFViewerApplication = { // Embedded PDF viewers should not be changing their parent page's title. return; } - const editorIndicator = - this._hasAnnotationEditors && !this.pdfRenderingQueue.printing; - document.title = `${editorIndicator ? "* " : ""}${title}`; + const hasChangesIndicator = + this._hasChanges() && !this.pdfRenderingQueue.printing; + document.title = `${hasChangesIndicator ? "* " : ""}${title}`; }, get _docFilename() { @@ -1129,12 +1129,12 @@ const PDFViewerApplication = { if ( (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC && !TESTING")) && - this.pdfDocument?.annotationStorage.size > 0 && + this._hasChanges() && this._annotationStorageModified ) { try { // Trigger saving, to prevent data loss in forms; see issue 12257. - await this.save(); + await this.downloadOrSave(); } catch { // Ignoring errors, to ensure that document closing won't break. } @@ -1315,9 +1315,15 @@ const PDFViewerApplication = { // a message and change PdfjsChild.sys.mjs to take it into account. const { classList } = this.appConfig.appContainer; classList.add("wait"); - await (this.pdfDocument?.annotationStorage.size > 0 - ? this.save() - : this.download()); + + const structuralChanges = this.pdfThumbnailViewer?.getStructuralChanges(); + if (structuralChanges) { + await this.onSavePages({ data: structuralChanges }); + } else { + await (this.pdfDocument?.annotationStorage.size > 0 + ? this.save() + : this.download()); + } classList.remove("wait"); }, @@ -1862,6 +1868,13 @@ const PDFViewerApplication = { } }, + _hasChanges() { + return ( + this.pdfDocument?.annotationStorage.size > 0 || + this.pdfThumbnailViewer?.hasStructuralChanges() + ); + }, + /** * @private */ @@ -1872,15 +1885,11 @@ const PDFViewerApplication = { const { annotationStorage } = pdfDocument; annotationStorage.onSetModified = () => { - window.addEventListener("beforeunload", beforeUnload); - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { this._annotationStorageModified = true; } }; annotationStorage.onResetModified = () => { - window.removeEventListener("beforeunload", beforeUnload); - if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { delete this._annotationStorageModified; } @@ -2185,11 +2194,7 @@ const PDFViewerApplication = { ); } eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts); - eventBus._on( - "savepageseditedpdf", - this.onSavePagesEditedPDF.bind(this), - opts - ); + eventBus._on("saveextractedpages", this.onSavePages.bind(this), opts); }, bindWindowEvents() { @@ -2270,6 +2275,9 @@ const PDFViewerApplication = { }, { signal } ); + window.addEventListener("beforeunload", onBeforeUnload.bind(this), { + signal, + }); if ( (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) && @@ -2368,7 +2376,7 @@ const PDFViewerApplication = { this.pdfViewer.onPagesEdited(data); }, - async onSavePagesEditedPDF({ data: extractParams }) { + async onSavePages({ data: extractParams }) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { return; } @@ -2876,6 +2884,15 @@ function closeEditorUndoBar(evt) { } } +function onBeforeUnload(evt) { + if (this._hasChanges()) { + evt.preventDefault(); + evt.returnValue = ""; + return false; + } + return true; +} + function onClick(evt) { closeSecondaryToolbar.call(this, evt); closeEditorUndoBar.call(this, evt); @@ -3230,10 +3247,4 @@ function onKeyDown(evt) { } } -function beforeUnload(evt) { - evt.preventDefault(); - evt.returnValue = ""; - return false; -} - export { PDFViewerApplication }; diff --git a/web/pdf_thumbnail_viewer.js b/web/pdf_thumbnail_viewer.js index 621065d45..3b1c3912d 100644 --- a/web/pdf_thumbnail_viewer.js +++ b/web/pdf_thumbnail_viewer.js @@ -175,12 +175,7 @@ class PDFThumbnailViewer { this._manageMenu = new Menu(menu, button, [copy, cut, del, saveAs]); this.#manageSaveAsButton = saveAs; - saveAs.addEventListener("click", () => { - this.eventBus.dispatch("savepageseditedpdf", { - source: this, - data: this.#pagesMapper.getPageMappingForSaving(), - }); - }); + saveAs.addEventListener("click", this.#saveExtractedPages.bind(this)); this.#manageDeleteButton = del; del.addEventListener("click", this.#deletePages.bind(this)); this.#manageCopyButton = copy; @@ -432,6 +427,14 @@ class PDFThumbnailViewer { return false; } + hasStructuralChanges() { + return this.#pagesMapper?.hasBeenAltered() || false; + } + + getStructuralChanges() { + return this.#pagesMapper?.getPageMappingForSaving() || null; + } + static #getScaleFactor(image) { return (PDFThumbnailViewer.#draggingScaleFactor ||= parseFloat( getComputedStyle(image).getPropertyValue("--thumbnail-dragging-scale") @@ -617,6 +620,15 @@ class PDFThumbnailViewer { this.#selectedPages.clear(); } + #saveExtractedPages() { + this.eventBus.dispatch("saveextractedpages", { + source: this, + data: this.#pagesMapper.extractPages(this.#selectedPages), + }); + this.#clearSelection(); + this.#toggleMenuEntries(false); + } + #copyPages(clearSelection = true) { const pageNumbersToCopy = (this.#copiedPageNumbers = Uint32Array.from( this.#selectedPages @@ -710,8 +722,8 @@ class PDFThumbnailViewer { } #updateMenuEntries() { - this.#manageSaveAsButton.disabled = !this.#pagesMapper.hasBeenAltered(); - this.#manageDeleteButton.disabled = + this.#manageSaveAsButton.disabled = + this.#manageDeleteButton.disabled = this.#manageCopyButton.disabled = this.#manageCutButton.disabled = !this.#selectedPages?.size;