diff --git a/package-lock.json b/package-lock.json index b29ab6f..81dcb08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "license": "Apache-2.0", "dependencies": { + "@neslinesli93/qpdf-wasm": "^0.3.0", "@tailwindcss/vite": "^4.1.14", "blob-stream": "^0.1.3", "cropperjs": "^1.6.1", @@ -2991,6 +2992,12 @@ "node": ">= 10" } }, + "node_modules/@neslinesli93/qpdf-wasm": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@neslinesli93/qpdf-wasm/-/qpdf-wasm-0.3.0.tgz", + "integrity": "sha512-ObyTmabopTMwN6AGwrEkmDbdmM5wnwlRdn9jnJYQ2KbBUPdPDcfNf1QkcWFOVD+qIgSqILz9nK51e22T+x+gkQ==", + "license": "ISC" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index d78ce8e..cd49a00 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "vitest": "^3.2.4" }, "dependencies": { + "@neslinesli93/qpdf-wasm": "^0.3.0", "@tailwindcss/vite": "^4.1.14", "blob-stream": "^0.1.3", "cropperjs": "^1.6.1", diff --git a/public/qpdf.wasm b/public/qpdf.wasm new file mode 100644 index 0000000..6c3d9f8 Binary files /dev/null and b/public/qpdf.wasm differ diff --git a/src/js/config/pdf-tools.ts b/src/js/config/pdf-tools.ts index de8baef..97b31ba 100644 --- a/src/js/config/pdf-tools.ts +++ b/src/js/config/pdf-tools.ts @@ -60,4 +60,5 @@ export const multiFileTools = [ 'heic-to-pdf', 'tiff-to-pdf', 'alternate-merge', + 'linearize', ]; diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 786e9ba..918fa81 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -383,6 +383,12 @@ export const categories = [ icon: 'ruler-dimension-line', subtitle: 'Standardize all pages to a uniform size.', }, + { + id: 'linearize', + name: 'Linearize PDF', + icon: 'zap', + subtitle: 'Optimize PDF for fast web viewing.', + }, { id: 'page-dimensions', name: 'Page Dimensions', diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index fe22f4c..df3788c 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -61,6 +61,7 @@ import { setupRemoveBlankPagesTool, } from './remove-blank-pages.js'; import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js'; +import { linearizePdf } from './linearize.js'; export const toolLogic = { merge: { process: merge, setup: setupMergeTool }, @@ -128,4 +129,5 @@ export const toolLogic = { process: alternateMerge, setup: setupAlternateMergeTool, }, + linearize: linearizePdf, }; diff --git a/src/js/logic/linearize.ts b/src/js/logic/linearize.ts new file mode 100644 index 0000000..6d797c2 --- /dev/null +++ b/src/js/logic/linearize.ts @@ -0,0 +1,122 @@ +import createModule from '@neslinesli93/qpdf-wasm'; +import { showLoader, hideLoader, showAlert } from '../ui'; +import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers'; +import { state } from '../state'; +import JSZip from 'jszip'; + +let qpdfInstance: any = null; + +async function initializeQpdf() { + if (qpdfInstance) { + return qpdfInstance; + } + showLoader('Initializing optimization engine...'); + try { + qpdfInstance = await createModule({ + locateFile: () => '/qpdf.wasm', + }); + } catch (error) { + console.error('Failed to initialize qpdf-wasm:', error); + showAlert( + 'Initialization Error', + 'Could not load the optimization engine. Please refresh the page and try again.' + ); + throw error; + } finally { + hideLoader(); + } + return qpdfInstance; +} + +export async function linearizePdf() { + // Check if there are files and at least one PDF + const pdfFiles = state.files.filter((file: File) => file.type === 'application/pdf'); + if (!pdfFiles || pdfFiles.length === 0) { + showAlert('No PDF Files', 'Please upload at least one PDF file.'); + return; + } + + showLoader('Optimizing PDFs for web view (linearizing)...'); + const zip = new JSZip(); // Create a JSZip instance + let qpdf: any; + let successCount = 0; + let errorCount = 0; + + try { + qpdf = await initializeQpdf(); + + for (let i = 0; i < pdfFiles.length; i++) { + const file = pdfFiles[i]; + const inputPath = `/input_${i}.pdf`; + const outputPath = `/output_${i}.pdf`; + + showLoader(`Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`); + + try { + const fileBuffer = await readFileAsArrayBuffer(file); + const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer); + + qpdf.FS.writeFile(inputPath, uint8Array); + + const args = [ + inputPath, + '--linearize', + outputPath, + ]; + + qpdf.callMain(args); + + const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' }); + if (!outputFile || outputFile.length === 0) { + console.error(`Linearization resulted in an empty file for ${file.name}.`); + throw new Error(`Processing failed for ${file.name}.`); + } + + zip.file(`linearized-${file.name}`, outputFile, { binary: true }); + successCount++; + + } catch (fileError: any) { + errorCount++; + console.error(`Failed to linearize ${file.name}:`, fileError); + // Optionally add an error marker/file to the zip? For now, we just skip. + } finally { + // Clean up WASM filesystem for this file + try { + if (qpdf?.FS) { + if (qpdf.FS.analyzePath(inputPath).exists) { + qpdf.FS.unlink(inputPath); + } + if (qpdf.FS.analyzePath(outputPath).exists) { + qpdf.FS.unlink(outputPath); + } + } + } catch (cleanupError) { + console.warn(`Failed to cleanup WASM FS for ${file.name}:`, cleanupError); + } + } + } + + if (successCount === 0) { + throw new Error('No PDF files could be linearized.'); + } + + showLoader('Generating ZIP file...'); + const zipBlob = await zip.generateAsync({ type: 'blob' }); + downloadFile(zipBlob, 'linearized-pdfs.zip'); + + let alertMessage = `${successCount} PDF(s) linearized successfully.`; + if (errorCount > 0) { + alertMessage += ` ${errorCount} file(s) failed.`; + } + showAlert('Processing Complete', alertMessage); + + } catch (error: any) { + console.error('Linearization process error:', error); + showAlert( + 'Linearization Failed', + `An error occurred: ${error.message || 'Unknown error'}.` + ); + } finally { + hideLoader(); + } +} \ No newline at end of file diff --git a/src/js/ui.ts b/src/js/ui.ts index d0e0780..47679d1 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -1846,4 +1846,12 @@ export const toolTemplates = { `, + +linearize: () => ` +
Optimize multiple PDFs for faster loading over the web. Files will be downloaded in a ZIP archive.
+ ${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 c517e9f..8e87e5f 100644 --- a/src/tests/pdf-tools.test.ts +++ b/src/tests/pdf-tools.test.ts @@ -61,7 +61,7 @@ describe('Tool Configuration Arrays', () => { }); it('should have the correct number of tools', () => { - expect(multiFileTools).toHaveLength(11); + expect(multiFileTools).toHaveLength(12); }); it('should not contain any duplicate tools', () => {