Skip to content

Commit a474e81

Browse files
committed
Add a way to extract some pages from a pdf (bug 2019682)
The user has to select some pages and then click on the "Save As" menu item in the Manage menu. If they modify the structure of the pdf (deleted, moved, copied pages), they have to use the usual save button.
1 parent 973add8 commit a474e81

4 files changed

Lines changed: 131 additions & 51 deletions

File tree

src/display/display_utils.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
BaseException,
1818
DrawOPS,
1919
FeatureTest,
20+
makeArr,
2021
MathClamp,
2122
shadow,
2223
stripPath,
@@ -1360,9 +1361,7 @@ class PagesMapper {
13601361
* Gets the current page mapping suitable for saving.
13611362
* @returns {Object} An object containing the page indices.
13621363
*/
1363-
getPageMappingForSaving() {
1364-
const idToPageNumber = this.#idToPageNumber;
1365-
1364+
getPageMappingForSaving(idToPageNumber = this.#idToPageNumber) {
13661365
// idToPageNumber maps used 1-based IDs to 1-based page numbers.
13671366
// For example if the final pdf contains page 3 twice and they are moved at
13681367
// page 1 and 4, then it contains:
@@ -1413,6 +1412,19 @@ class PagesMapper {
14131412
return extractParams;
14141413
}
14151414

1415+
extractPages(extractedPageNumbers) {
1416+
extractedPageNumbers = Array.from(extractedPageNumbers).sort(
1417+
(a, b) => a - b
1418+
);
1419+
const usedIds = new Map();
1420+
for (let i = 0, ii = extractedPageNumbers.length; i < ii; i++) {
1421+
const id = this.getPageId(extractedPageNumbers[i]);
1422+
const usedPageNumbers = usedIds.getOrInsertComputed(id, makeArr);
1423+
usedPageNumbers.push(i + 1);
1424+
}
1425+
return this.getPageMappingForSaving(usedIds);
1426+
}
1427+
14161428
/**
14171429
* Gets the previous page number for a given page number.
14181430
* @param {number} pageNumber

test/integration/reorganize_pages_spec.mjs

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -614,24 +614,14 @@ describe("Reorganize Pages View", () => {
614614
10
615615
);
616616

617-
const handleSaveAs = await createPromise(page, resolve => {
618-
window.PDFViewerApplication.eventBus.on(
619-
"savepageseditedpdf",
620-
({ data }) => {
621-
resolve(Array.from(data[0].pageIndices));
622-
},
623-
{
624-
once: true,
625-
}
626-
);
617+
const handleSave = await createPromise(page, resolve => {
618+
window.PDFViewerApplication.onSavePages = async ({ data }) => {
619+
resolve(Array.from(data[0].pageIndices));
620+
};
627621
});
628622

629-
await page.click("#viewsManagerStatusActionButton");
630-
await page.waitForSelector("#viewsManagerStatusActionSaveAs", {
631-
visible: true,
632-
});
633-
await page.click("#viewsManagerStatusActionSaveAs");
634-
const pageIndices = await awaitPromise(handleSaveAs);
623+
await waitAndClick(page, "#downloadButton");
624+
const pageIndices = await awaitPromise(handleSave);
635625
expect(pageIndices)
636626
.withContext(`In ${browserName}`)
637627
.toEqual([
@@ -1041,4 +1031,59 @@ describe("Reorganize Pages View", () => {
10411031
);
10421032
});
10431033
});
1034+
1035+
describe("Extract some pages from a pdf", () => {
1036+
let pages;
1037+
1038+
beforeEach(async () => {
1039+
pages = await loadAndWait(
1040+
"page_with_number.pdf",
1041+
"#viewsManagerToggleButton",
1042+
"page-fit",
1043+
null,
1044+
{ enableSplitMerge: true }
1045+
);
1046+
});
1047+
1048+
afterEach(async () => {
1049+
await closePages(pages);
1050+
});
1051+
1052+
it("should check that the pages are correctly extracted", async () => {
1053+
await Promise.all(
1054+
pages.map(async ([browserName, page]) => {
1055+
await waitForThumbnailVisible(page, 1);
1056+
await waitAndClick(
1057+
page,
1058+
`.thumbnail:has(${getThumbnailSelector(1)}) input`
1059+
);
1060+
await waitAndClick(
1061+
page,
1062+
`.thumbnail:has(${getThumbnailSelector(3)}) input`
1063+
);
1064+
1065+
const handleSaveAs = await createPromise(page, resolve => {
1066+
window.PDFViewerApplication.eventBus.on(
1067+
"saveextractedpages",
1068+
({ data }) => {
1069+
resolve(data);
1070+
},
1071+
{
1072+
once: true,
1073+
}
1074+
);
1075+
});
1076+
1077+
await page.click("#viewsManagerStatusActionButton");
1078+
await waitAndClick(page, "#viewsManagerStatusActionSaveAs");
1079+
const pagesData = await awaitPromise(handleSaveAs);
1080+
expect(pagesData)
1081+
.withContext(`In ${browserName}`)
1082+
.toEqual([
1083+
{ document: null, pageIndices: [0, 1], includePages: [0, 2] },
1084+
]);
1085+
})
1086+
);
1087+
});
1088+
});
10441089
});

web/app.js

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,9 +1072,9 @@ const PDFViewerApplication = {
10721072
// Embedded PDF viewers should not be changing their parent page's title.
10731073
return;
10741074
}
1075-
const editorIndicator =
1076-
this._hasAnnotationEditors && !this.pdfRenderingQueue.printing;
1077-
document.title = `${editorIndicator ? "* " : ""}${title}`;
1075+
const hasChangesIndicator =
1076+
this._hasChanges() && !this.pdfRenderingQueue.printing;
1077+
document.title = `${hasChangesIndicator ? "* " : ""}${title}`;
10781078
},
10791079

10801080
get _docFilename() {
@@ -1129,12 +1129,12 @@ const PDFViewerApplication = {
11291129
if (
11301130
(typeof PDFJSDev === "undefined" ||
11311131
PDFJSDev.test("GENERIC && !TESTING")) &&
1132-
this.pdfDocument?.annotationStorage.size > 0 &&
1132+
this._hasChanges() &&
11331133
this._annotationStorageModified
11341134
) {
11351135
try {
11361136
// Trigger saving, to prevent data loss in forms; see issue 12257.
1137-
await this.save();
1137+
await this.downloadOrSave();
11381138
} catch {
11391139
// Ignoring errors, to ensure that document closing won't break.
11401140
}
@@ -1315,9 +1315,15 @@ const PDFViewerApplication = {
13151315
// a message and change PdfjsChild.sys.mjs to take it into account.
13161316
const { classList } = this.appConfig.appContainer;
13171317
classList.add("wait");
1318-
await (this.pdfDocument?.annotationStorage.size > 0
1319-
? this.save()
1320-
: this.download());
1318+
1319+
const structuralChanges = this.pdfThumbnailViewer?.getStructuralChanges();
1320+
if (structuralChanges) {
1321+
await this.onSavePages({ data: structuralChanges });
1322+
} else {
1323+
await (this.pdfDocument?.annotationStorage.size > 0
1324+
? this.save()
1325+
: this.download());
1326+
}
13211327
classList.remove("wait");
13221328
},
13231329

@@ -1862,6 +1868,13 @@ const PDFViewerApplication = {
18621868
}
18631869
},
18641870

1871+
_hasChanges() {
1872+
return (
1873+
this.pdfDocument?.annotationStorage.size > 0 ||
1874+
this.pdfThumbnailViewer?.hasStructuralChanges()
1875+
);
1876+
},
1877+
18651878
/**
18661879
* @private
18671880
*/
@@ -1872,15 +1885,11 @@ const PDFViewerApplication = {
18721885
const { annotationStorage } = pdfDocument;
18731886

18741887
annotationStorage.onSetModified = () => {
1875-
window.addEventListener("beforeunload", beforeUnload);
1876-
18771888
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
18781889
this._annotationStorageModified = true;
18791890
}
18801891
};
18811892
annotationStorage.onResetModified = () => {
1882-
window.removeEventListener("beforeunload", beforeUnload);
1883-
18841893
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
18851894
delete this._annotationStorageModified;
18861895
}
@@ -2185,11 +2194,7 @@ const PDFViewerApplication = {
21852194
);
21862195
}
21872196
eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts);
2188-
eventBus._on(
2189-
"savepageseditedpdf",
2190-
this.onSavePagesEditedPDF.bind(this),
2191-
opts
2192-
);
2197+
eventBus._on("saveextractedpages", this.onSavePages.bind(this), opts);
21932198
},
21942199

21952200
bindWindowEvents() {
@@ -2270,6 +2275,9 @@ const PDFViewerApplication = {
22702275
},
22712276
{ signal }
22722277
);
2278+
window.addEventListener("beforeunload", onBeforeUnload.bind(this), {
2279+
signal,
2280+
});
22732281

22742282
if (
22752283
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
@@ -2368,7 +2376,7 @@ const PDFViewerApplication = {
23682376
this.pdfViewer.onPagesEdited(data);
23692377
},
23702378

2371-
async onSavePagesEditedPDF({ data: extractParams }) {
2379+
async onSavePages({ data: extractParams }) {
23722380
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
23732381
return;
23742382
}
@@ -2876,6 +2884,15 @@ function closeEditorUndoBar(evt) {
28762884
}
28772885
}
28782886

2887+
function onBeforeUnload(evt) {
2888+
if (this._hasChanges()) {
2889+
evt.preventDefault();
2890+
evt.returnValue = "";
2891+
return false;
2892+
}
2893+
return true;
2894+
}
2895+
28792896
function onClick(evt) {
28802897
closeSecondaryToolbar.call(this, evt);
28812898
closeEditorUndoBar.call(this, evt);
@@ -3230,10 +3247,4 @@ function onKeyDown(evt) {
32303247
}
32313248
}
32323249

3233-
function beforeUnload(evt) {
3234-
evt.preventDefault();
3235-
evt.returnValue = "";
3236-
return false;
3237-
}
3238-
32393250
export { PDFViewerApplication };

web/pdf_thumbnail_viewer.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,7 @@ class PDFThumbnailViewer {
175175

176176
this._manageMenu = new Menu(menu, button, [copy, cut, del, saveAs]);
177177
this.#manageSaveAsButton = saveAs;
178-
saveAs.addEventListener("click", () => {
179-
this.eventBus.dispatch("savepageseditedpdf", {
180-
source: this,
181-
data: this.#pagesMapper.getPageMappingForSaving(),
182-
});
183-
});
178+
saveAs.addEventListener("click", this.#saveExtractedPages.bind(this));
184179
this.#manageDeleteButton = del;
185180
del.addEventListener("click", this.#deletePages.bind(this));
186181
this.#manageCopyButton = copy;
@@ -432,6 +427,14 @@ class PDFThumbnailViewer {
432427
return false;
433428
}
434429

430+
hasStructuralChanges() {
431+
return this.#pagesMapper?.hasBeenAltered() || false;
432+
}
433+
434+
getStructuralChanges() {
435+
return this.#pagesMapper?.getPageMappingForSaving() || null;
436+
}
437+
435438
static #getScaleFactor(image) {
436439
return (PDFThumbnailViewer.#draggingScaleFactor ||= parseFloat(
437440
getComputedStyle(image).getPropertyValue("--thumbnail-dragging-scale")
@@ -617,6 +620,15 @@ class PDFThumbnailViewer {
617620
this.#selectedPages.clear();
618621
}
619622

623+
#saveExtractedPages() {
624+
this.eventBus.dispatch("saveextractedpages", {
625+
source: this,
626+
data: this.#pagesMapper.extractPages(this.#selectedPages),
627+
});
628+
this.#clearSelection();
629+
this.#toggleMenuEntries(false);
630+
}
631+
620632
#copyPages(clearSelection = true) {
621633
const pageNumbersToCopy = (this.#copiedPageNumbers = Uint32Array.from(
622634
this.#selectedPages
@@ -710,8 +722,8 @@ class PDFThumbnailViewer {
710722
}
711723

712724
#updateMenuEntries() {
713-
this.#manageSaveAsButton.disabled = !this.#pagesMapper.hasBeenAltered();
714-
this.#manageDeleteButton.disabled =
725+
this.#manageSaveAsButton.disabled =
726+
this.#manageDeleteButton.disabled =
715727
this.#manageCopyButton.disabled =
716728
this.#manageCutButton.disabled =
717729
!this.#selectedPages?.size;

0 commit comments

Comments
 (0)