Skip to content

Commit 68cca32

Browse files
authored
Merge pull request #20785 from calixteman/extract_pages
Add a way to extract some pages from a pdf (bug 2019682)
2 parents 286b0cc + a474e81 commit 68cca32

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([
@@ -1083,4 +1073,59 @@ describe("Reorganize Pages View", () => {
10831073
);
10841074
});
10851075
});
1076+
1077+
describe("Extract some pages from a pdf", () => {
1078+
let pages;
1079+
1080+
beforeEach(async () => {
1081+
pages = await loadAndWait(
1082+
"page_with_number.pdf",
1083+
"#viewsManagerToggleButton",
1084+
"page-fit",
1085+
null,
1086+
{ enableSplitMerge: true }
1087+
);
1088+
});
1089+
1090+
afterEach(async () => {
1091+
await closePages(pages);
1092+
});
1093+
1094+
it("should check that the pages are correctly extracted", async () => {
1095+
await Promise.all(
1096+
pages.map(async ([browserName, page]) => {
1097+
await waitForThumbnailVisible(page, 1);
1098+
await waitAndClick(
1099+
page,
1100+
`.thumbnail:has(${getThumbnailSelector(1)}) input`
1101+
);
1102+
await waitAndClick(
1103+
page,
1104+
`.thumbnail:has(${getThumbnailSelector(3)}) input`
1105+
);
1106+
1107+
const handleSaveAs = await createPromise(page, resolve => {
1108+
window.PDFViewerApplication.eventBus.on(
1109+
"saveextractedpages",
1110+
({ data }) => {
1111+
resolve(data);
1112+
},
1113+
{
1114+
once: true,
1115+
}
1116+
);
1117+
});
1118+
1119+
await page.click("#viewsManagerStatusActionButton");
1120+
await waitAndClick(page, "#viewsManagerStatusActionSaveAs");
1121+
const pagesData = await awaitPromise(handleSaveAs);
1122+
expect(pagesData)
1123+
.withContext(`In ${browserName}`)
1124+
.toEqual([
1125+
{ document: null, pageIndices: [0, 1], includePages: [0, 2] },
1126+
]);
1127+
})
1128+
);
1129+
});
1130+
});
10861131
});

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
@@ -713,8 +725,8 @@ class PDFThumbnailViewer {
713725
}
714726

715727
#updateMenuEntries() {
716-
this.#manageSaveAsButton.disabled = !this.#pagesMapper.hasBeenAltered();
717-
this.#manageDeleteButton.disabled =
728+
this.#manageSaveAsButton.disabled =
729+
this.#manageDeleteButton.disabled =
718730
this.#manageCopyButton.disabled =
719731
this.#manageCutButton.disabled =
720732
!this.#selectedPages?.size;

0 commit comments

Comments
 (0)