mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-03 00:31:01 +02:00
Allow merging several PDFs at once via the picker or drag-and-drop
This commit is contained in:
parent
e7661983f7
commit
6b92ad5924
@ -45,20 +45,24 @@ import path from "path";
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
async function createPDFDataTransfer(page, filename) {
|
||||
const pdfPath = path.join(__dirname, "../pdfs", filename);
|
||||
const pdfData = fs.readFileSync(pdfPath).toString("base64");
|
||||
return page.evaluateHandle(
|
||||
(data, name) => {
|
||||
const transfer = new DataTransfer();
|
||||
const view = Uint8Array.fromBase64(data);
|
||||
const file = new File([view], name, { type: "application/pdf" });
|
||||
transfer.items.add(file);
|
||||
return transfer;
|
||||
},
|
||||
pdfData,
|
||||
filename
|
||||
);
|
||||
async function createPDFDataTransfer(page, ...filenames) {
|
||||
const pdfData = filenames.map(filename => {
|
||||
const pdfPath = path.join(__dirname, "../pdfs", filename);
|
||||
return {
|
||||
data: fs.readFileSync(pdfPath).toString("base64"),
|
||||
filename,
|
||||
};
|
||||
});
|
||||
return page.evaluateHandle(data => {
|
||||
const transfer = new DataTransfer();
|
||||
for (const { data: base64, filename } of data) {
|
||||
const view = Uint8Array.fromBase64(base64);
|
||||
transfer.items.add(
|
||||
new File([view], filename, { type: "application/pdf" })
|
||||
);
|
||||
}
|
||||
return transfer;
|
||||
}, pdfData);
|
||||
}
|
||||
|
||||
async function waitForThumbnailVisible(page, pageNums) {
|
||||
@ -3243,6 +3247,62 @@ describe("Reorganize Pages View", () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should merge several PDFs selected at once", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForThumbnailVisible(page, 1);
|
||||
|
||||
// Navigate to page 2 so the merged PDFs are inserted after it.
|
||||
await page.evaluate(() => {
|
||||
window.PDFViewerApplication.page = 2;
|
||||
});
|
||||
await page.waitForFunction(
|
||||
() => window.PDFViewerApplication.page === 2
|
||||
);
|
||||
await waitAndClick(page, getThumbnailSelector(2));
|
||||
|
||||
const handleMerged = await createPromise(page, resolve => {
|
||||
window.PDFViewerApplication.eventBus.on(
|
||||
"thumbnailsloaded",
|
||||
resolve,
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
|
||||
const picker = await page.$("#viewsManagerAddFilePicker");
|
||||
const pdfPath = path.join(
|
||||
__dirname,
|
||||
"../pdfs/three_pages_with_number.pdf"
|
||||
);
|
||||
// Upload two PDFs in a single picker selection.
|
||||
await picker.uploadFile(pdfPath, pdfPath);
|
||||
await awaitPromise(handleMerged);
|
||||
|
||||
// Original 3 pages + 2 * 3 merged pages = 9 pages total.
|
||||
await page.waitForFunction(
|
||||
() => parseInt(document.getElementById("pageNumber").max, 10) === 9
|
||||
);
|
||||
|
||||
// Focus must move to the first newly inserted page (page 3, since
|
||||
// we merged after page 2).
|
||||
await page.waitForFunction(
|
||||
() => window.PDFViewerApplication.page === 3
|
||||
);
|
||||
|
||||
// Pages 1–2 of the original, then both merged copies (in selection
|
||||
// order), then page 3 of the original shifted to the end.
|
||||
await waitForHavingContents(page, [1, 2, 1, 2, 3, 1, 2, 3, 3]);
|
||||
|
||||
// All 6 newly inserted pages must be selected.
|
||||
await waitForTextToBe(
|
||||
page,
|
||||
"#viewsManagerStatusActionLabel",
|
||||
`${FSI}6${PDI} selected`
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Drag-and-drop PDF merge", () => {
|
||||
@ -3375,5 +3435,65 @@ describe("Reorganize Pages View", () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should merge several dropped PDFs at once", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForThumbnailVisible(page, [1, 2, 3]);
|
||||
|
||||
const dataTransfer = await createPDFDataTransfer(
|
||||
page,
|
||||
"three_pages_with_number.pdf",
|
||||
"three_pages_with_number.pdf"
|
||||
);
|
||||
|
||||
const handleMerged = await createPromise(page, resolve => {
|
||||
window.PDFViewerApplication.eventBus.on(
|
||||
"thumbnailsloaded",
|
||||
resolve,
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
const filesLength = await page.evaluate(
|
||||
(transfer, selector) => {
|
||||
const target = document.querySelector(selector);
|
||||
const { left, top, width, height } =
|
||||
target.getBoundingClientRect();
|
||||
const clientX = left + width / 4;
|
||||
const clientY = top + (3 * height) / 4;
|
||||
for (const type of ["dragenter", "dragover", "drop"]) {
|
||||
target.dispatchEvent(
|
||||
new DragEvent(type, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX,
|
||||
clientY,
|
||||
dataTransfer: transfer,
|
||||
})
|
||||
);
|
||||
}
|
||||
return transfer.files.length;
|
||||
},
|
||||
dataTransfer,
|
||||
getThumbnailSelector(2)
|
||||
);
|
||||
expect(filesLength).withContext(`In ${browserName}`).toBe(2);
|
||||
await awaitPromise(handleMerged);
|
||||
|
||||
await page.waitForFunction(
|
||||
() => parseInt(document.getElementById("pageNumber").max, 10) === 9
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => window.PDFViewerApplication.page === 3
|
||||
);
|
||||
await waitForHavingContents(page, [1, 2, 1, 2, 3, 1, 2, 3, 3]);
|
||||
await waitForTextToBe(
|
||||
page,
|
||||
"#viewsManagerStatusActionLabel",
|
||||
`${FSI}6${PDI} selected`
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -79,7 +79,7 @@ const SPACE_FOR_DRAG_MARKER_WHEN_NO_NEXT_ELEMENT = 15;
|
||||
* @property {Object} [waitingBar] - The waiting bar elements shown during
|
||||
* long-running operations.
|
||||
* @property {Object} [addFileComponent] - The file picker and button used to
|
||||
* add a PDF file to merge with the current one.
|
||||
* add one or more PDF files to merge with the current one.
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -321,9 +321,9 @@ class PDFThumbnailViewer {
|
||||
if (this.#enableMerge && addFileComponent) {
|
||||
const { picker, button } = addFileComponent;
|
||||
picker.addEventListener("change", () => {
|
||||
const file = picker.files?.[0];
|
||||
if (file) {
|
||||
this.#mergeFile(file, this._currentPageNumber - 1);
|
||||
const files = Array.from(picker.files ?? []);
|
||||
if (files.length) {
|
||||
this.#mergeFiles(files, this._currentPageNumber - 1);
|
||||
}
|
||||
});
|
||||
button.addEventListener("click", () => {
|
||||
@ -351,23 +351,32 @@ class PDFThumbnailViewer {
|
||||
this.renderingQueue.renderHighestPriority();
|
||||
}
|
||||
|
||||
async #mergeFile(file, insertAfter) {
|
||||
if (file.type !== "application/pdf") {
|
||||
const magic = await file.slice(0, 5).text();
|
||||
if (magic !== "%PDF-") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
async #mergeFiles(files, insertAfter) {
|
||||
this.#toggleBar("waiting", "pdfjs-views-manager-waiting-for-file");
|
||||
const buffer = await file.bytes();
|
||||
const buffers = [];
|
||||
for (const file of files) {
|
||||
if (file.type !== "application/pdf") {
|
||||
const magic = await file.slice(0, 5).text();
|
||||
if (magic !== "%PDF-") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
buffers.push(await file.bytes());
|
||||
}
|
||||
if (buffers.length === 0) {
|
||||
this.#toggleBar("status");
|
||||
return;
|
||||
}
|
||||
const pagesCount = this.#pagesMapper.pagesNumber;
|
||||
const data = this.hasStructuralChanges()
|
||||
? this.getStructuralChanges()
|
||||
: [{ document: null }];
|
||||
data.push({
|
||||
document: buffer,
|
||||
insertAfter,
|
||||
});
|
||||
for (const buffer of buffers) {
|
||||
data.push({
|
||||
document: buffer,
|
||||
insertAfter,
|
||||
});
|
||||
}
|
||||
this.eventBus._on(
|
||||
"pagesloaded",
|
||||
() => {
|
||||
@ -1579,7 +1588,7 @@ class PDFThumbnailViewer {
|
||||
// the only available signal. Matches the existing global drop handler
|
||||
// in app.js. Files with no MIME (e.g. some macOS sources) are rejected
|
||||
// here to keep the "copy" cursor honest; if needed, drop-time magic-byte
|
||||
// validation in #mergeFile would still catch a permissive variant.
|
||||
// validation in #mergeFiles would still catch a permissive variant.
|
||||
for (const item of dataTransfer.items) {
|
||||
if (item.kind === "file" && item.type === "application/pdf") {
|
||||
return true;
|
||||
@ -1668,7 +1677,7 @@ class PDFThumbnailViewer {
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
const files = Array.from(e.dataTransfer.files ?? []);
|
||||
// If no dragover ever ran (e.g. instant drop), compute the index from
|
||||
// the drop event itself so we don't fall through to a stale fallback.
|
||||
if (isNaN(this.#lastDraggedOverIndex) && this.#thumbnailsPositions) {
|
||||
@ -1682,8 +1691,8 @@ class PDFThumbnailViewer {
|
||||
? -1
|
||||
: this.#lastDraggedOverIndex;
|
||||
this.#endExternalFileDrag();
|
||||
if (file) {
|
||||
this.#mergeFile(file, insertAfter);
|
||||
if (files.length) {
|
||||
this.#mergeFiles(files, insertAfter);
|
||||
}
|
||||
},
|
||||
{ signal }
|
||||
|
||||
@ -191,7 +191,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||
hidden="true"
|
||||
>
|
||||
<span data-l10n-id="pdfjs-views-manager-add-file-button-label"></span>
|
||||
<input id="viewsManagerAddFilePicker" type="file" accept="application/pdf" />
|
||||
<input id="viewsManagerAddFilePicker" type="file" accept="application/pdf" multiple />
|
||||
</button>
|
||||
<button
|
||||
id="viewsManagerCurrentOutlineButton"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user