feat(pdf-tools): add alternate merge tool for mixing pdf pages

Implement new tool that allows users to combine PDFs by alternating pages from each document. Includes UI components, logic for processing, and test coverage.

- Add new tool to configuration arrays and categories
- Create UI with drag-and-drop file ordering
- Implement core logic for alternating pages
- Add comprehensive unit tests
This commit is contained in:
abdullahalam123
2025-10-16 12:35:43 +05:30
parent a82148c253
commit 48baad9bf9
8 changed files with 291 additions and 2 deletions

View File

@@ -0,0 +1,107 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument } from 'pdf-lib';
import Sortable from 'sortablejs';
const alternateMergeState = {
pdfDocs: {},
};
export async function setupAlternateMergeTool() {
const optionsDiv = document.getElementById('alternate-merge-options');
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
const fileList = document.getElementById('alternate-file-list');
if (!optionsDiv || !processBtn || !fileList) return;
optionsDiv.classList.remove('hidden');
processBtn.disabled = false;
processBtn.onclick = alternateMerge;
fileList.innerHTML = '';
alternateMergeState.pdfDocs = {};
showLoader('Loading PDF documents...');
try {
for (const file of state.files) {
const pdfBytes = await readFileAsArrayBuffer(file);
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(pdfBytes as ArrayBuffer, {
ignoreEncryption: true
});
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
const li = document.createElement('li');
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
li.dataset.fileName = file.name;
const infoDiv = document.createElement('div');
infoDiv.className = 'flex items-center gap-2 truncate';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white';
nameSpan.textContent = file.name;
const pagesSpan = document.createElement('span');
pagesSpan.className = 'text-sm text-gray-400 flex-shrink-0';
pagesSpan.textContent = `(${pageCount} pages)`;
infoDiv.append(nameSpan, pagesSpan);
const dragHandle = document.createElement('div');
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded';
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
li.append(infoDiv, dragHandle);
fileList.appendChild(li);
}
Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
});
} catch (error) {
showAlert('Error', 'Failed to load one or more PDF files. They may be corrupted or password-protected.');
console.error(error);
} finally {
hideLoader();
}
}
export async function alternateMerge() {
if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
showAlert('Not Enough Files', 'Please upload at least two PDF files to alternate and mix.');
return;
}
showLoader('Alternating and mixing pages...');
try {
const newPdfDoc = await PDFDocument.create();
const fileList = document.getElementById('alternate-file-list');
const sortedFileNames = Array.from(fileList.children).map(li => (li as HTMLElement).dataset.fileName);
const loadedDocs = sortedFileNames.map(name => alternateMergeState.pdfDocs[name]);
const pageCounts = loadedDocs.map(doc => doc.getPageCount());
const maxPages = Math.max(...pageCounts);
for (let i = 0; i < maxPages; i++) {
for (const doc of loadedDocs) {
if (i < doc.getPageCount()) {
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
newPdfDoc.addPage(copiedPage);
}
}
}
const mergedPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), 'alternated-mixed.pdf');
showAlert('Success', 'PDFs have been mixed successfully!');
} catch (e) {
console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.');
} finally {
hideLoader();
}
}