Merge pull request #20582 from calixteman/reorg_save

Add a manage button in the thumbnail view in order to save an edited pdf (bug 2010830)
This commit is contained in:
calixteman 2026-01-26 19:25:50 +01:00 committed by GitHub
commit 5505201930
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 177 additions and 10 deletions

View File

@ -1182,10 +1182,39 @@ class PagesMapper {
// Finally insert the moved pages.
pageNumberToId.set(mappedPagesToMove, adjustedTarget);
let hasChanged = false;
for (let i = 0, ii = pagesNumber; i < ii; i++) {
idToPageNumber[pageNumberToId[i] - 1] = i + 1;
const id = pageNumberToId[i];
hasChanged ||= id !== i + 1;
idToPageNumber[id - 1] = i + 1;
}
this.#updateListeners();
if (!hasChanged) {
// Reset.
this.pagesNumber = 0;
}
}
/**
* Checks if the page mappings have been altered from their initial state.
* @returns {boolean} True if the mappings have been altered, false otherwise.
*/
hasBeenAltered() {
return PagesMapper.#pageNumberToId !== null;
}
/**
* Gets the current page mapping suitable for saving.
* @returns {Object} An object containing the page indices.
*/
getPageMappingForSaving() {
// Saving is index-based.
return {
pageIndices: PagesMapper.#idToPageNumber
? PagesMapper.#idToPageNumber.map(x => x - 1)
: null,
};
}
getPrevPageNumber(pageNumber) {

View File

@ -568,4 +568,66 @@ describe("Reorganize Pages View", () => {
);
});
});
describe("Save a pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"page_with_number.pdf",
"#viewsManagerToggleButton",
"1",
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should check that a save is triggered", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
await page.waitForSelector("#viewsManagerStatusActionButton", {
visible: true,
});
const rect1 = await getRect(page, getThumbnailSelector(1));
const rect2 = await getRect(page, getThumbnailSelector(2));
await dragAndDrop(
page,
getThumbnailSelector(1),
[[0, rect2.y - rect1.y + rect2.height / 2]],
10
);
const handleSaveAs = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"savepageseditedpdf",
({ data }) => {
resolve(Array.from(data.pageIndices));
},
{
once: true,
}
);
});
await page.click("#viewsManagerStatusActionButton");
await page.waitForSelector("#viewsManagerStatusActionSaveAs", {
visible: true,
});
await page.click("#viewsManagerStatusActionSaveAs");
const pageIndices = await awaitPromise(handleSaveAs);
expect(pageIndices)
.withContext(`In ${browserName}`)
.toEqual([
1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
]);
})
);
});
});
});

View File

@ -124,6 +124,13 @@ describe("PDF Thumbnail View", () => {
.withContext(`In ${browserName}`)
.toBe(true);
await kbFocusNext(page);
expect(
await isElementFocused(page, "#viewsManagerStatusActionButton")
)
.withContext(`In ${browserName}`)
.toBe(true);
await kbFocusNext(page);
expect(
await isElementFocused(

View File

@ -605,6 +605,7 @@ const PDFViewerApplication = {
abortSignal,
enableHWA,
enableSplitMerge: AppOptions.get("enableSplitMerge"),
manageMenu: appConfig.viewsManager.manageMenu,
});
renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
}
@ -2194,6 +2195,11 @@ const PDFViewerApplication = {
this.onBeforePagesEdited.bind(this),
opts
);
eventBus._on(
"savepageseditedpdf",
this.onSavePagesEditedPDF.bind(this),
opts
);
},
bindWindowEvents() {
@ -2376,6 +2382,35 @@ const PDFViewerApplication = {
this.pdfViewer.onPagesEdited(data);
},
async onSavePagesEditedPDF({
data: { includePages, excludePages, pageIndices },
}) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
return;
}
if (!this.pdfDocument) {
return;
}
const pageInfo = {
document: null, // For now, no merge.
includePages,
excludePages,
pageIndices,
};
const modifiedPdfBytes = await this.pdfDocument.extractPages([pageInfo]);
if (!modifiedPdfBytes) {
console.error(
"Something wrong happened when saving the edited PDF.\nPlease file a bug."
);
return;
}
this.downloadManager.download(
modifiedPdfBytes,
this._downloadUrl,
this._docFilename
);
},
_accumulateTicks(ticks, prop) {
// If the direction changed, reset the accumulated ticks.
if ((this[prop] > 0 && ticks < 0) || (this[prop] < 0 && ticks > 0)) {

View File

@ -28,6 +28,7 @@ import {
watchScroll,
} from "./ui_utils.js";
import { MathClamp, noContextMenu, PagesMapper, stopEvent } from "pdfjs-lib";
import { Menu } from "./menu.js";
import { PDFThumbnailView } from "./pdf_thumbnail_view.js";
const SCROLL_OPTIONS = {
@ -67,6 +68,8 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15;
* rendering. The default value is `false`.
* @property {boolean} [enableSplitMerge] - Enables split and merge features.
* The default value is `false`.
* @property {Object} [manageMenu] - The menu elements to manage saving edited
* PDF.
*/
/**
@ -109,6 +112,8 @@ class PDFThumbnailViewer {
#pagesMapper = PagesMapper.instance;
#manageSaveAsButton = null;
/**
* @param {PDFThumbnailViewerOptions} options
*/
@ -123,6 +128,7 @@ class PDFThumbnailViewer {
abortSignal,
enableHWA,
enableSplitMerge,
manageMenu,
}) {
this.scrollableContainer = container.parentElement;
this.container = container;
@ -135,6 +141,20 @@ class PDFThumbnailViewer {
this.enableHWA = enableHWA || false;
this.#enableSplitMerge = enableSplitMerge || false;
if (this.#enableSplitMerge && manageMenu) {
const { button, menu, copy, cut, delete: del, saveAs } = manageMenu;
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(),
});
});
} else {
manageMenu.button.hidden = true;
}
this.scroll = watchScroll(
this.scrollableContainer,
this.#scrollUpdated.bind(this),
@ -519,10 +539,16 @@ class PDFThumbnailViewer {
selectedPages.clear();
this.#pageNumberToRemove = NaN;
this.eventBus.dispatch("pagesedited", {
source: this,
pagesMapper,
});
const isIdentity = (this.#manageSaveAsButton.disabled =
!this.#pagesMapper.hasBeenAltered());
if (!isIdentity) {
this.eventBus.dispatch("pagesedited", {
source: this,
pagesMapper,
index: newIndex,
pagesToMove,
});
}
const newCurrentPageNumber = pagesMapper.getPageNumber(newCurrentPageId);
setTimeout(() => {

View File

@ -187,7 +187,7 @@ See https://github.com/adobe-type-tools/cmap-resources
</button>
</div>
<div id="viewsManagerStatus">
<div id="viewsManagerStatusAction" class="hidden">
<div id="viewsManagerStatusAction">
<span
id="viewsManagerStatusActionLabel"
class="viewsManagerStatusLabel"
@ -207,22 +207,22 @@ See https://github.com/adobe-type-tools/cmap-resources
</button>
<menu id="viewsManagerStatusActionOptions" class="popupMenu">
<li>
<button id="viewsManagerStatusActionCopy" class="noIcon" role="menuitem" type="button" tabindex="0">
<button id="viewsManagerStatusActionCopy" class="noIcon" role="menuitem" type="button" tabindex="0" disabled>
<span data-l10n-id="pdfjs-views-manager-pages-status-copy-button-label"></span>
</button>
</li>
<li>
<button id="viewsManagerStatusActionCut" class="noIcon" role="menuitem" type="button" tabindex="0">
<button id="viewsManagerStatusActionCut" class="noIcon" role="menuitem" type="button" tabindex="0" disabled>
<span data-l10n-id="pdfjs-views-manager-pages-status-cut-button-label"></span>
</button>
</li>
<li>
<button id="viewsManagerStatusActionDelete" class="noIcon" role="menuitem" type="button" tabindex="0">
<button id="viewsManagerStatusActionDelete" class="noIcon" role="menuitem" type="button" tabindex="0" disabled>
<span data-l10n-id="pdfjs-views-manager-pages-status-delete-button-label"></span>
</button>
</li>
<li>
<button id="viewsManagerStatusActionSaveAs" class="noIcon" role="menuitem" type="button" tabindex="0">
<button id="viewsManagerStatusActionSaveAs" class="noIcon" role="menuitem" type="button" tabindex="0" disabled>
<span data-l10n-id="pdfjs-views-manager-pages-status-save-as-button-label"></span>
</button>
</li>

View File

@ -132,6 +132,14 @@ function getViewerConfiguration() {
viewsManagerHeaderLabel: document.getElementById(
"viewsManagerHeaderLabel"
),
manageMenu: {
button: document.getElementById("viewsManagerStatusActionButton"),
menu: document.getElementById("viewsManagerStatusActionOptions"),
copy: document.getElementById("viewsManagerStatusActionCopy"),
cut: document.getElementById("viewsManagerStatusActionCut"),
delete: document.getElementById("viewsManagerStatusActionDelete"),
saveAs: document.getElementById("viewsManagerStatusActionSaveAs"),
},
},
findBar: {
bar: document.getElementById("findbar"),