Merge pull request #20785 from calixteman/extract_pages

Add a way to extract some pages from a pdf (bug 2019682)
This commit is contained in:
calixteman 2026-03-04 08:44:13 +01:00 committed by GitHub
commit 68cca32e20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 131 additions and 51 deletions

View File

@ -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

View File

@ -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([
@ -1083,4 +1073,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] },
]);
})
);
});
});
});

View File

@ -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 };

View File

@ -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
@ -713,8 +725,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;