diff --git a/src/js/config/pdf-tools.ts b/src/js/config/pdf-tools.ts index 17ba38e..574b64f 100644 --- a/src/js/config/pdf-tools.ts +++ b/src/js/config/pdf-tools.ts @@ -18,7 +18,6 @@ export const singlePdfLoadTools = [ 'add-header-footer', 'invert-colors', 'view-metadata', - 'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', @@ -62,4 +61,5 @@ export const multiFileTools = [ 'tiff-to-pdf', 'alternate-merge', 'linearize', + 'reverse-pages', ]; diff --git a/src/js/handlers/fileHandler.ts b/src/js/handlers/fileHandler.ts index dd558ff..b5cfc68 100644 --- a/src/js/handlers/fileHandler.ts +++ b/src/js/handlers/fileHandler.ts @@ -25,8 +25,8 @@ async function handleSinglePdfUpload(toolId, file) { showLoader('Loading PDF...'); try { const pdfBytes = await readFileAsArrayBuffer(file); - state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { - ignoreEncryption: true, + state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { + ignoreEncryption: true }); hideLoader(); @@ -324,7 +324,7 @@ async function handleSinglePdfUpload(toolId, file) { } async function handleMultiFileUpload(toolId) { - if (toolId === 'merge' || toolId === 'alternate-merge') { + if (toolId === 'merge' || toolId === 'alternate-merge' || toolId === 'reverse-pages') { const pdfFilesUnloaded: File[] = []; state.files.forEach((file) => { diff --git a/src/js/logic/reverse-pages.ts b/src/js/logic/reverse-pages.ts index 95a2aca..2b47278 100644 --- a/src/js/logic/reverse-pages.ts +++ b/src/js/logic/reverse-pages.ts @@ -3,29 +3,38 @@ import { downloadFile } from '../utils/helpers.js'; import { state } from '../state.js'; import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +import JSZip from 'jszip'; export async function reversePages() { - if (!state.pdfDoc) { + const pdfDocs = state.files.filter((file: File) => file.type === 'application/pdf'); + if (!pdfDocs.length) { showAlert('Error', 'PDF not loaded.'); return; } showLoader('Reversing page order...'); try { - const newPdf = await PDFLibDocument.create(); - const pageCount = state.pdfDoc.getPageCount(); - const reversedIndices = Array.from( - { length: pageCount }, - (_, i) => pageCount - 1 - i - ); + const zip = new JSZip(); + for (let j = 0; j < pdfDocs.length; j++) { + const file = pdfDocs[j]; + const arrayBuffer = await file.arrayBuffer(); + const pdfDoc = await PDFLibDocument.load(arrayBuffer); + const newPdf = await PDFLibDocument.create(); + const pageCount = pdfDoc.getPageCount(); + const reversedIndices = Array.from( + { length: pageCount }, + (_, i) => pageCount - 1 - i + ); - const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices); - copiedPages.forEach((page: any) => newPdf.addPage(page)); + const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices); + copiedPages.forEach((page: any) => newPdf.addPage(page)); - const newPdfBytes = await newPdf.save(); - downloadFile( - new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), - 'reversed.pdf' - ); + const newPdfBytes = await newPdf.save(); + const originalName = file.name.replace(/\.pdf$/i, ''); + const fileName = `${originalName}_reversed.pdf`; + zip.file(fileName, newPdfBytes); + } + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, 'reversed_pdfs.zip'); } catch (e) { console.error(e); showAlert('Error', 'Could not reverse the PDF pages.'); diff --git a/src/js/ui.ts b/src/js/ui.ts index bdd9f39..ffad5d8 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -969,7 +969,7 @@ export const toolTemplates = { 'reverse-pages': () => `
Flip the order of all pages in your document, making the last page the first.
- ${createFileInputHTML()} + ${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })} `, diff --git a/src/tests/pdf-tools.test.ts b/src/tests/pdf-tools.test.ts index a5436d8..6370e75 100644 --- a/src/tests/pdf-tools.test.ts +++ b/src/tests/pdf-tools.test.ts @@ -19,7 +19,7 @@ describe('Tool Configuration Arrays', () => { it('should have the correct number of tools', () => { // This acts as a snapshot test to catch unexpected additions/removals. - expect(singlePdfLoadTools).toHaveLength(40); + expect(singlePdfLoadTools).toHaveLength(39); }); it('should not contain any duplicate tools', () => { @@ -61,7 +61,7 @@ describe('Tool Configuration Arrays', () => { }); it('should have the correct number of tools', () => { - expect(multiFileTools).toHaveLength(12); + expect(multiFileTools).toHaveLength(13); }); it('should not contain any duplicate tools', () => { diff --git a/src/tests/reversePages.multi.test.ts b/src/tests/reversePages.multi.test.ts new file mode 100644 index 0000000..c25bce1 --- /dev/null +++ b/src/tests/reversePages.multi.test.ts @@ -0,0 +1,114 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +import { reversePages } from '../js/logic/reverse-pages'; +import { state } from '../js/state'; +import * as helpers from '../js/utils/helpers'; +import * as ui from '../js/ui'; +import JSZip from 'jszip'; + +vi.mock('../js/ui', () => ({ + showLoader: vi.fn(), + hideLoader: vi.fn(), + showAlert: vi.fn(), +})); + +vi.mock('../js/utils/helpers', () => ({ + downloadFile: vi.fn(), +})); + +vi.mock('pdf-lib', () => ({ + PDFDocument: { + create: vi.fn(), + load: vi.fn().mockResolvedValue({ + getPageCount: vi.fn(() => 2), + copyPages: vi.fn((_, indices) => + Promise.resolve(indices.map((i) => ({ page: `page-${i}` }))) + ), + }), + }, +})); + +describe('reversePages - multi PDF support', () => { + let mockNewDoc: any; + + beforeEach(() => { + state.files = []; // ✅ now using files, not pdfDocs + + mockNewDoc = { + copyPages: vi.fn((doc: any, indices: number[]) => + Promise.resolve(indices.map((i: number) => ({ page: `page-${i}` }))) + ), + addPage: vi.fn(), + save: vi.fn(() => Promise.resolve(new Uint8Array([1, 2, 3]))), + }; + vi.mocked(PDFLibDocument.create).mockResolvedValue(mockNewDoc); + + vi.mocked(helpers.downloadFile).mockImplementation(() => {}); + vi.mocked(ui.showLoader).mockImplementation(() => {}); + vi.mocked(ui.hideLoader).mockImplementation(() => {}); + vi.mocked(ui.showAlert).mockImplementation(() => {}); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should reverse pages for multiple PDFs and create a zip', async () => { + const mockFile = (name: string) => ({ + name, + type: 'application/pdf', + arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)), + }); + + state.files = [mockFile('a.pdf'), mockFile('b.pdf')]; // ✅ now matches function + + await reversePages(); + + expect(helpers.downloadFile).toHaveBeenCalledWith(expect.any(Blob), 'reversed_pdfs.zip'); + expect(mockNewDoc.copyPages).toHaveBeenCalledTimes(2); + expect(mockNewDoc.addPage).toHaveBeenCalled(); + expect(mockNewDoc.save).toHaveBeenCalledTimes(2); + }); + + it('should handle empty PDF list gracefully', async () => { + state.files = []; + await reversePages(); + expect(ui.showAlert).toHaveBeenCalledWith('Error', 'PDF not loaded.'); + }); + + it('should handle PDF creation errors', async () => { + vi.mocked(PDFLibDocument.create).mockRejectedValue(new Error('Create failed')); + state.files = [ + { name: 'x.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) }, + ]; + + await reversePages(); + + expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.'); + expect(ui.hideLoader).toHaveBeenCalled(); + }); + + it('should handle PDF processing errors', async () => { + mockNewDoc.copyPages.mockRejectedValue(new Error('Copy failed')); + state.files = [ + { name: 'y.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) }, + ]; + + await reversePages(); + + expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.'); + expect(ui.hideLoader).toHaveBeenCalled(); + }); + + it('should handle save errors', async () => { + mockNewDoc.save.mockRejectedValue(new Error('Save failed')); + state.files = [ + { name: 'z.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) }, + ]; + + await reversePages(); + + expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.'); + expect(ui.hideLoader).toHaveBeenCalled(); + }); +}); \ No newline at end of file