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, BaseException,
DrawOPS, DrawOPS,
FeatureTest, FeatureTest,
makeArr,
MathClamp, MathClamp,
shadow, shadow,
stripPath, stripPath,
@ -1360,9 +1361,7 @@ class PagesMapper {
* Gets the current page mapping suitable for saving. * Gets the current page mapping suitable for saving.
* @returns {Object} An object containing the page indices. * @returns {Object} An object containing the page indices.
*/ */
getPageMappingForSaving() { getPageMappingForSaving(idToPageNumber = this.#idToPageNumber) {
const idToPageNumber = this.#idToPageNumber;
// idToPageNumber maps used 1-based IDs to 1-based page numbers. // 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 // For example if the final pdf contains page 3 twice and they are moved at
// page 1 and 4, then it contains: // page 1 and 4, then it contains:
@ -1413,6 +1412,19 @@ class PagesMapper {
return extractParams; 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. * Gets the previous page number for a given page number.
* @param {number} pageNumber * @param {number} pageNumber

View File

@ -614,24 +614,14 @@ describe("Reorganize Pages View", () => {
10 10
); );
const handleSaveAs = await createPromise(page, resolve => { const handleSave = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on( window.PDFViewerApplication.onSavePages = async ({ data }) => {
"savepageseditedpdf", resolve(Array.from(data[0].pageIndices));
({ data }) => { };
resolve(Array.from(data[0].pageIndices));
},
{
once: true,
}
);
}); });
await page.click("#viewsManagerStatusActionButton"); await waitAndClick(page, "#downloadButton");
await page.waitForSelector("#viewsManagerStatusActionSaveAs", { const pageIndices = await awaitPromise(handleSave);
visible: true,
});
await page.click("#viewsManagerStatusActionSaveAs");
const pageIndices = await awaitPromise(handleSaveAs);
expect(pageIndices) expect(pageIndices)
.withContext(`In ${browserName}`) .withContext(`In ${browserName}`)
.toEqual([ .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. // Embedded PDF viewers should not be changing their parent page's title.
return; return;
} }
const editorIndicator = const hasChangesIndicator =
this._hasAnnotationEditors && !this.pdfRenderingQueue.printing; this._hasChanges() && !this.pdfRenderingQueue.printing;
document.title = `${editorIndicator ? "* " : ""}${title}`; document.title = `${hasChangesIndicator ? "* " : ""}${title}`;
}, },
get _docFilename() { get _docFilename() {
@ -1129,12 +1129,12 @@ const PDFViewerApplication = {
if ( if (
(typeof PDFJSDev === "undefined" || (typeof PDFJSDev === "undefined" ||
PDFJSDev.test("GENERIC && !TESTING")) && PDFJSDev.test("GENERIC && !TESTING")) &&
this.pdfDocument?.annotationStorage.size > 0 && this._hasChanges() &&
this._annotationStorageModified this._annotationStorageModified
) { ) {
try { try {
// Trigger saving, to prevent data loss in forms; see issue 12257. // Trigger saving, to prevent data loss in forms; see issue 12257.
await this.save(); await this.downloadOrSave();
} catch { } catch {
// Ignoring errors, to ensure that document closing won't break. // 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. // a message and change PdfjsChild.sys.mjs to take it into account.
const { classList } = this.appConfig.appContainer; const { classList } = this.appConfig.appContainer;
classList.add("wait"); classList.add("wait");
await (this.pdfDocument?.annotationStorage.size > 0
? this.save() const structuralChanges = this.pdfThumbnailViewer?.getStructuralChanges();
: this.download()); if (structuralChanges) {
await this.onSavePages({ data: structuralChanges });
} else {
await (this.pdfDocument?.annotationStorage.size > 0
? this.save()
: this.download());
}
classList.remove("wait"); classList.remove("wait");
}, },
@ -1862,6 +1868,13 @@ const PDFViewerApplication = {
} }
}, },
_hasChanges() {
return (
this.pdfDocument?.annotationStorage.size > 0 ||
this.pdfThumbnailViewer?.hasStructuralChanges()
);
},
/** /**
* @private * @private
*/ */
@ -1872,15 +1885,11 @@ const PDFViewerApplication = {
const { annotationStorage } = pdfDocument; const { annotationStorage } = pdfDocument;
annotationStorage.onSetModified = () => { annotationStorage.onSetModified = () => {
window.addEventListener("beforeunload", beforeUnload);
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
this._annotationStorageModified = true; this._annotationStorageModified = true;
} }
}; };
annotationStorage.onResetModified = () => { annotationStorage.onResetModified = () => {
window.removeEventListener("beforeunload", beforeUnload);
if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) {
delete this._annotationStorageModified; delete this._annotationStorageModified;
} }
@ -2185,11 +2194,7 @@ const PDFViewerApplication = {
); );
} }
eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts); eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts);
eventBus._on( eventBus._on("saveextractedpages", this.onSavePages.bind(this), opts);
"savepageseditedpdf",
this.onSavePagesEditedPDF.bind(this),
opts
);
}, },
bindWindowEvents() { bindWindowEvents() {
@ -2270,6 +2275,9 @@ const PDFViewerApplication = {
}, },
{ signal } { signal }
); );
window.addEventListener("beforeunload", onBeforeUnload.bind(this), {
signal,
});
if ( if (
(typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) && (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) &&
@ -2368,7 +2376,7 @@ const PDFViewerApplication = {
this.pdfViewer.onPagesEdited(data); this.pdfViewer.onPagesEdited(data);
}, },
async onSavePagesEditedPDF({ data: extractParams }) { async onSavePages({ data: extractParams }) {
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING")) {
return; 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) { function onClick(evt) {
closeSecondaryToolbar.call(this, evt); closeSecondaryToolbar.call(this, evt);
closeEditorUndoBar.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 }; export { PDFViewerApplication };

View File

@ -175,12 +175,7 @@ class PDFThumbnailViewer {
this._manageMenu = new Menu(menu, button, [copy, cut, del, saveAs]); this._manageMenu = new Menu(menu, button, [copy, cut, del, saveAs]);
this.#manageSaveAsButton = saveAs; this.#manageSaveAsButton = saveAs;
saveAs.addEventListener("click", () => { saveAs.addEventListener("click", this.#saveExtractedPages.bind(this));
this.eventBus.dispatch("savepageseditedpdf", {
source: this,
data: this.#pagesMapper.getPageMappingForSaving(),
});
});
this.#manageDeleteButton = del; this.#manageDeleteButton = del;
del.addEventListener("click", this.#deletePages.bind(this)); del.addEventListener("click", this.#deletePages.bind(this));
this.#manageCopyButton = copy; this.#manageCopyButton = copy;
@ -432,6 +427,14 @@ class PDFThumbnailViewer {
return false; return false;
} }
hasStructuralChanges() {
return this.#pagesMapper?.hasBeenAltered() || false;
}
getStructuralChanges() {
return this.#pagesMapper?.getPageMappingForSaving() || null;
}
static #getScaleFactor(image) { static #getScaleFactor(image) {
return (PDFThumbnailViewer.#draggingScaleFactor ||= parseFloat( return (PDFThumbnailViewer.#draggingScaleFactor ||= parseFloat(
getComputedStyle(image).getPropertyValue("--thumbnail-dragging-scale") getComputedStyle(image).getPropertyValue("--thumbnail-dragging-scale")
@ -617,6 +620,15 @@ class PDFThumbnailViewer {
this.#selectedPages.clear(); this.#selectedPages.clear();
} }
#saveExtractedPages() {
this.eventBus.dispatch("saveextractedpages", {
source: this,
data: this.#pagesMapper.extractPages(this.#selectedPages),
});
this.#clearSelection();
this.#toggleMenuEntries(false);
}
#copyPages(clearSelection = true) { #copyPages(clearSelection = true) {
const pageNumbersToCopy = (this.#copiedPageNumbers = Uint32Array.from( const pageNumbersToCopy = (this.#copiedPageNumbers = Uint32Array.from(
this.#selectedPages this.#selectedPages
@ -713,8 +725,8 @@ class PDFThumbnailViewer {
} }
#updateMenuEntries() { #updateMenuEntries() {
this.#manageSaveAsButton.disabled = !this.#pagesMapper.hasBeenAltered(); this.#manageSaveAsButton.disabled =
this.#manageDeleteButton.disabled = this.#manageDeleteButton.disabled =
this.#manageCopyButton.disabled = this.#manageCopyButton.disabled =
this.#manageCutButton.disabled = this.#manageCutButton.disabled =
!this.#selectedPages?.size; !this.#selectedPages?.size;