Skip to content

Commit dd6a0c6

Browse files
committed
Add a manage button in the thumbnail view in order to save an edited pdf (bug 2010830)
1 parent 07a4aab commit dd6a0c6

7 files changed

Lines changed: 177 additions & 10 deletions

File tree

src/display/display_utils.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1182,10 +1182,39 @@ class PagesMapper {
11821182
// Finally insert the moved pages.
11831183
pageNumberToId.set(mappedPagesToMove, adjustedTarget);
11841184

1185+
let hasChanged = false;
11851186
for (let i = 0, ii = pagesNumber; i < ii; i++) {
1186-
idToPageNumber[pageNumberToId[i] - 1] = i + 1;
1187+
const id = pageNumberToId[i];
1188+
hasChanged ||= id !== i + 1;
1189+
idToPageNumber[id - 1] = i + 1;
11871190
}
11881191
this.#updateListeners();
1192+
1193+
if (!hasChanged) {
1194+
// Reset.
1195+
this.pagesNumber = 0;
1196+
}
1197+
}
1198+
1199+
/**
1200+
* Checks if the page mappings have been altered from their initial state.
1201+
* @returns {boolean} True if the mappings have been altered, false otherwise.
1202+
*/
1203+
hasBeenAltered() {
1204+
return PagesMapper.#pageNumberToId !== null;
1205+
}
1206+
1207+
/**
1208+
* Gets the current page mapping suitable for saving.
1209+
* @returns {Object} An object containing the page indices.
1210+
*/
1211+
getPageMappingForSaving() {
1212+
// Saving is index-based.
1213+
return {
1214+
pageIndices: PagesMapper.#idToPageNumber
1215+
? PagesMapper.#idToPageNumber.map(x => x - 1)
1216+
: null,
1217+
};
11891218
}
11901219

11911220
getPrevPageNumber(pageNumber) {

test/integration/reorganize_pages_spec.mjs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,4 +568,66 @@ describe("Reorganize Pages View", () => {
568568
);
569569
});
570570
});
571+
572+
describe("Save a pdf", () => {
573+
let pages;
574+
575+
beforeEach(async () => {
576+
pages = await loadAndWait(
577+
"page_with_number.pdf",
578+
"#viewsManagerToggleButton",
579+
"1",
580+
null,
581+
{ enableSplitMerge: true }
582+
);
583+
});
584+
585+
afterEach(async () => {
586+
await closePages(pages);
587+
});
588+
589+
it("should check that a save is triggered", async () => {
590+
await Promise.all(
591+
pages.map(async ([browserName, page]) => {
592+
await waitForThumbnailVisible(page, 1);
593+
await page.waitForSelector("#viewsManagerStatusActionButton", {
594+
visible: true,
595+
});
596+
const rect1 = await getRect(page, getThumbnailSelector(1));
597+
const rect2 = await getRect(page, getThumbnailSelector(2));
598+
599+
await dragAndDrop(
600+
page,
601+
getThumbnailSelector(1),
602+
[[0, rect2.y - rect1.y + rect2.height / 2]],
603+
10
604+
);
605+
606+
const handleSaveAs = await createPromise(page, resolve => {
607+
window.PDFViewerApplication.eventBus.on(
608+
"savepageseditedpdf",
609+
({ data }) => {
610+
resolve(Array.from(data.pageIndices));
611+
},
612+
{
613+
once: true,
614+
}
615+
);
616+
});
617+
618+
await page.click("#viewsManagerStatusActionButton");
619+
await page.waitForSelector("#viewsManagerStatusActionSaveAs", {
620+
visible: true,
621+
});
622+
await page.click("#viewsManagerStatusActionSaveAs");
623+
const pageIndices = await awaitPromise(handleSaveAs);
624+
expect(pageIndices)
625+
.withContext(`In ${browserName}`)
626+
.toEqual([
627+
1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
628+
]);
629+
})
630+
);
631+
});
632+
});
571633
});

test/integration/thumbnail_view_spec.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ describe("PDF Thumbnail View", () => {
124124
.withContext(`In ${browserName}`)
125125
.toBe(true);
126126

127+
await kbFocusNext(page);
128+
expect(
129+
await isElementFocused(page, "#viewsManagerStatusActionButton")
130+
)
131+
.withContext(`In ${browserName}`)
132+
.toBe(true);
133+
127134
await kbFocusNext(page);
128135
expect(
129136
await isElementFocused(

web/app.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,7 @@ const PDFViewerApplication = {
605605
abortSignal,
606606
enableHWA,
607607
enableSplitMerge: AppOptions.get("enableSplitMerge"),
608+
manageMenu: appConfig.viewsManager.manageMenu,
608609
});
609610
renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
610611
}
@@ -2194,6 +2195,11 @@ const PDFViewerApplication = {
21942195
this.onBeforePagesEdited.bind(this),
21952196
opts
21962197
);
2198+
eventBus._on(
2199+
"savepageseditedpdf",
2200+
this.onSavePagesEditedPDF.bind(this),
2201+
opts
2202+
);
21972203
},
21982204

21992205
bindWindowEvents() {
@@ -2376,6 +2382,35 @@ const PDFViewerApplication = {
23762382
this.pdfViewer.onPagesEdited(data);
23772383
},
23782384

2385+
async onSavePagesEditedPDF({
2386+
data: { includePages, excludePages, pageIndices },
2387+
}) {
2388+
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
2389+
return;
2390+
}
2391+
if (!this.pdfDocument) {
2392+
return;
2393+
}
2394+
const pageInfo = {
2395+
document: null, // For now, no merge.
2396+
includePages,
2397+
excludePages,
2398+
pageIndices,
2399+
};
2400+
const modifiedPdfBytes = await this.pdfDocument.extractPages([pageInfo]);
2401+
if (!modifiedPdfBytes) {
2402+
console.error(
2403+
"Something wrong happened when saving the edited PDF.\nPlease file a bug."
2404+
);
2405+
return;
2406+
}
2407+
this.downloadManager.download(
2408+
modifiedPdfBytes,
2409+
this._downloadUrl,
2410+
this._docFilename
2411+
);
2412+
},
2413+
23792414
_accumulateTicks(ticks, prop) {
23802415
// If the direction changed, reset the accumulated ticks.
23812416
if ((this[prop] > 0 && ticks < 0) || (this[prop] < 0 && ticks > 0)) {

web/pdf_thumbnail_viewer.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
watchScroll,
2929
} from "./ui_utils.js";
3030
import { MathClamp, noContextMenu, PagesMapper, stopEvent } from "pdfjs-lib";
31+
import { Menu } from "./menu.js";
3132
import { PDFThumbnailView } from "./pdf_thumbnail_view.js";
3233

3334
const SCROLL_OPTIONS = {
@@ -67,6 +68,8 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15;
6768
* rendering. The default value is `false`.
6869
* @property {boolean} [enableSplitMerge] - Enables split and merge features.
6970
* The default value is `false`.
71+
* @property {Object} [manageMenu] - The menu elements to manage saving edited
72+
* PDF.
7073
*/
7174

7275
/**
@@ -109,6 +112,8 @@ class PDFThumbnailViewer {
109112

110113
#pagesMapper = PagesMapper.instance;
111114

115+
#manageSaveAsButton = null;
116+
112117
/**
113118
* @param {PDFThumbnailViewerOptions} options
114119
*/
@@ -123,6 +128,7 @@ class PDFThumbnailViewer {
123128
abortSignal,
124129
enableHWA,
125130
enableSplitMerge,
131+
manageMenu,
126132
}) {
127133
this.scrollableContainer = container.parentElement;
128134
this.container = container;
@@ -135,6 +141,20 @@ class PDFThumbnailViewer {
135141
this.enableHWA = enableHWA || false;
136142
this.#enableSplitMerge = enableSplitMerge || false;
137143

144+
if (this.#enableSplitMerge && manageMenu) {
145+
const { button, menu, copy, cut, delete: del, saveAs } = manageMenu;
146+
this._manageMenu = new Menu(menu, button, [copy, cut, del, saveAs]);
147+
this.#manageSaveAsButton = saveAs;
148+
saveAs.addEventListener("click", () => {
149+
this.eventBus.dispatch("savepageseditedpdf", {
150+
source: this,
151+
data: this.#pagesMapper.getPageMappingForSaving(),
152+
});
153+
});
154+
} else {
155+
manageMenu.button.hidden = true;
156+
}
157+
138158
this.scroll = watchScroll(
139159
this.scrollableContainer,
140160
this.#scrollUpdated.bind(this),
@@ -519,10 +539,16 @@ class PDFThumbnailViewer {
519539
selectedPages.clear();
520540
this.#pageNumberToRemove = NaN;
521541

522-
this.eventBus.dispatch("pagesedited", {
523-
source: this,
524-
pagesMapper,
525-
});
542+
const isIdentity = (this.#manageSaveAsButton.disabled =
543+
!this.#pagesMapper.hasBeenAltered());
544+
if (!isIdentity) {
545+
this.eventBus.dispatch("pagesedited", {
546+
source: this,
547+
pagesMapper,
548+
index: newIndex,
549+
pagesToMove,
550+
});
551+
}
526552

527553
const newCurrentPageNumber = pagesMapper.getPageNumber(newCurrentPageId);
528554
setTimeout(() => {

web/viewer.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@
187187
</button>
188188
</div>
189189
<div id="viewsManagerStatus">
190-
<div id="viewsManagerStatusAction" class="hidden">
190+
<div id="viewsManagerStatusAction">
191191
<span
192192
id="viewsManagerStatusActionLabel"
193193
class="viewsManagerStatusLabel"
@@ -207,22 +207,22 @@
207207
</button>
208208
<menu id="viewsManagerStatusActionOptions" class="popupMenu">
209209
<li>
210-
<button id="viewsManagerStatusActionCopy" class="noIcon" role="menuitem" type="button" tabindex="0">
210+
<button id="viewsManagerStatusActionCopy" class="noIcon" role="menuitem" type="button" tabindex="0" disabled>
211211
<span data-l10n-id="pdfjs-views-manager-pages-status-copy-button-label"></span>
212212
</button>
213213
</li>
214214
<li>
215-
<button id="viewsManagerStatusActionCut" class="noIcon" role="menuitem" type="button" tabindex="0">
215+
<button id="viewsManagerStatusActionCut" class="noIcon" role="menuitem" type="button" tabindex="0" disabled>
216216
<span data-l10n-id="pdfjs-views-manager-pages-status-cut-button-label"></span>
217217
</button>
218218
</li>
219219
<li>
220-
<button id="viewsManagerStatusActionDelete" class="noIcon" role="menuitem" type="button" tabindex="0">
220+
<button id="viewsManagerStatusActionDelete" class="noIcon" role="menuitem" type="button" tabindex="0" disabled>
221221
<span data-l10n-id="pdfjs-views-manager-pages-status-delete-button-label"></span>
222222
</button>
223223
</li>
224224
<li>
225-
<button id="viewsManagerStatusActionSaveAs" class="noIcon" role="menuitem" type="button" tabindex="0">
225+
<button id="viewsManagerStatusActionSaveAs" class="noIcon" role="menuitem" type="button" tabindex="0" disabled>
226226
<span data-l10n-id="pdfjs-views-manager-pages-status-save-as-button-label"></span>
227227
</button>
228228
</li>

web/viewer.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ function getViewerConfiguration() {
132132
viewsManagerHeaderLabel: document.getElementById(
133133
"viewsManagerHeaderLabel"
134134
),
135+
manageMenu: {
136+
button: document.getElementById("viewsManagerStatusActionButton"),
137+
menu: document.getElementById("viewsManagerStatusActionOptions"),
138+
copy: document.getElementById("viewsManagerStatusActionCopy"),
139+
cut: document.getElementById("viewsManagerStatusActionCut"),
140+
delete: document.getElementById("viewsManagerStatusActionDelete"),
141+
saveAs: document.getElementById("viewsManagerStatusActionSaveAs"),
142+
},
135143
},
136144
findBar: {
137145
bar: document.getElementById("findbar"),

0 commit comments

Comments
 (0)