diff --git a/package-lock.json b/package-lock.json index 6b46b8d..516ca30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bento-pdf", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bento-pdf", - "version": "1.1.0", + "version": "1.1.1", "license": "Apache-2.0", "dependencies": { "@fontsource/cedarville-cursive": "^5.2.7", diff --git a/src/js/logic/decrypt.ts b/src/js/logic/decrypt.ts index 758ad09..ed1008f 100644 --- a/src/js/logic/decrypt.ts +++ b/src/js/logic/decrypt.ts @@ -1,79 +1,126 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; import { state } from '../state.js'; -import PDFDocument from 'pdfkit/js/pdfkit.standalone'; -import blobStream from 'blob-stream'; -import * as pdfjsLib from 'pdfjs-dist'; +import createModule from '@neslinesli93/qpdf-wasm'; + +let qpdfInstance: any = null; + +async function initializeQpdf() { + if (qpdfInstance) { + return qpdfInstance; + } + showLoader('Initializing decryption 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 decryption engine. Please refresh the page and try again.' + ); + throw error; + } finally { + hideLoader(); + } + return qpdfInstance; +} export async function decrypt() { const file = state.files[0]; - // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message - const password = document.getElementById('password-input').value; - if (!password.trim()) { + const password = ( + document.getElementById('password-input') as HTMLInputElement + )?.value; + + if (!password?.trim()) { showAlert('Input Required', 'Please enter the PDF password.'); return; } + const inputPath = '/input.pdf'; + const outputPath = '/output.pdf'; + let qpdf: any; + try { - showLoader('Preparing to process...'); - const pdfData = (await readFileAsArrayBuffer(file)) as ArrayBuffer; - const pdf = await pdfjsLib.getDocument({ - data: pdfData, - password: password, - }).promise; - const numPages = pdf.numPages; - const pageImages = []; + showLoader('Initializing decryption...'); + qpdf = await initializeQpdf(); - for (let i = 1; i <= numPages; i++) { - document.getElementById('loader-text').textContent = - `Processing page ${i} of ${numPages}...`; - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 2.0 }); - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - canvas.height = viewport.height; - canvas.width = viewport.width; - await page.render({ - canvasContext: context, - viewport: viewport, - canvas: canvas, - }).promise; - pageImages.push({ - data: canvas.toDataURL('image/jpeg', 0.8), - width: viewport.width, - height: viewport.height, - }); + showLoader('Reading encrypted PDF...'); + const fileBuffer = await readFileAsArrayBuffer(file); + const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer); + + qpdf.FS.writeFile(inputPath, uint8Array); + + showLoader('Decrypting PDF...'); + + const args = [inputPath, '--password=' + password, '--decrypt', outputPath]; + + try { + qpdf.callMain(args); + } catch (qpdfError: any) { + console.error('qpdf execution error:', qpdfError); + + if ( + qpdfError.message?.includes('invalid password') || + qpdfError.message?.includes('password') + ) { + throw new Error('INVALID_PASSWORD'); + } + throw qpdfError; } - document.getElementById('loader-text').textContent = - 'Building unlocked PDF...'; - const doc = new PDFDocument({ - size: [pageImages[0].width, pageImages[0].height], - }); - const stream = doc.pipe(blobStream()); - for (let i = 0; i < pageImages.length; i++) { - if (i > 0) - doc.addPage({ size: [pageImages[i].width, pageImages[i].height] }); - doc.image(pageImages[i].data, 0, 0, { - width: pageImages[i].width, - height: pageImages[i].height, - }); - } - doc.end(); + showLoader('Preparing download...'); + const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' }); - stream.on('finish', function () { - const blob = stream.toBlob('application/pdf'); - downloadFile(blob, `unlocked-${file.name}`); - hideLoader(); - showAlert('Success', 'Decryption complete! Your download has started.'); - }); - } catch (error) { + if (!outputFile || outputFile.length === 0) { + throw new Error('Decryption resulted in an empty file.'); + } + + const blob = new Blob([outputFile], { type: 'application/pdf' }); + downloadFile(blob, `unlocked-${file.name}`); + + hideLoader(); + showAlert( + 'Success', + 'PDF decrypted successfully! Your download has started.' + ); + } catch (error: any) { console.error('Error during PDF decryption:', error); hideLoader(); - if (error.name === 'PasswordException') { - showAlert('Incorrect Password', 'The password you entered is incorrect.'); + + if (error.message === 'INVALID_PASSWORD') { + showAlert( + 'Incorrect Password', + 'The password you entered is incorrect. Please try again.' + ); + } else if (error.message?.includes('password')) { + showAlert( + 'Password Error', + 'Unable to decrypt the PDF with the provided password.' + ); } else { - showAlert('Error', 'An error occurred. The PDF might be corrupted.'); + showAlert( + 'Decryption Failed', + `An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}` + ); + } + } finally { + try { + if (qpdf?.FS) { + try { + qpdf.FS.unlink(inputPath); + } catch (e) { + console.warn('Failed to unlink input file:', e); + } + try { + qpdf.FS.unlink(outputPath); + } catch (e) { + console.warn('Failed to unlink output file:', e); + } + } + } catch (cleanupError) { + console.warn('Failed to cleanup WASM FS:', cleanupError); } } } diff --git a/src/js/logic/encrypt.ts b/src/js/logic/encrypt.ts index f9f8e9d..26717ef 100644 --- a/src/js/logic/encrypt.ts +++ b/src/js/logic/encrypt.ts @@ -1,86 +1,140 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; import { state } from '../state.js'; -import PDFDocument from 'pdfkit/js/pdfkit.standalone'; -import blobStream from 'blob-stream'; -import * as pdfjsLib from 'pdfjs-dist'; +import createModule from '@neslinesli93/qpdf-wasm'; + +let qpdfInstance: any = null; + +async function initializeQpdf() { + if (qpdfInstance) { + return qpdfInstance; + } + showLoader('Initializing encryption 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 encryption engine. Please refresh the page and try again.' + ); + throw error; + } finally { + hideLoader(); + } + return qpdfInstance; +} export async function encrypt() { const file = state.files[0]; - const password = ( - document.getElementById('password-input') as HTMLInputElement - ).value; - if (!password.trim()) { - showAlert('Input Required', 'Please enter a password.'); + const userPassword = + (document.getElementById('user-password-input') as HTMLInputElement) + ?.value || ''; + const ownerPassword = + (document.getElementById('owner-password-input') as HTMLInputElement) + ?.value || ''; + + if (!userPassword) { + showAlert('Input Required', 'Please enter a user password.'); return; } + const inputPath = '/input.pdf'; + const outputPath = '/output.pdf'; + let qpdf: any; + try { - showLoader('Preparing to process...'); - const pdfData = await readFileAsArrayBuffer(file); - const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer }) - .promise; - const numPages = pdf.numPages; - const pageImages = []; + showLoader('Initializing encryption...'); + qpdf = await initializeQpdf(); - for (let i = 1; i <= numPages; i++) { - document.getElementById('loader-text').textContent = - `Processing page ${i} of ${numPages}...`; - const page = await pdf.getPage(i); - const viewport = page.getViewport({ scale: 2.0 }); - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - canvas.height = viewport.height; - canvas.width = viewport.width; - await page.render({ - canvasContext: context, - viewport: viewport, - canvas: canvas, - }).promise; - pageImages.push({ - data: canvas.toDataURL('image/jpeg', 0.8), - width: viewport.width, - height: viewport.height, - }); + showLoader('Reading PDF...'); + const fileBuffer = await readFileAsArrayBuffer(file); + const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer); + + qpdf.FS.writeFile(inputPath, uint8Array); + + showLoader('Encrypting PDF with 256-bit AES...'); + + const args = [ + inputPath, + '--encrypt', + userPassword, + ownerPassword, // Can be empty + '256', + ]; + + if (ownerPassword) { + args.push( + '--modify=none', + '--extract=n', + '--print=none', + '--accessibility=n', + '--annotate=n', + '--assemble=n', + '--form=n', + '--modify-other=n', + ); } - document.getElementById('loader-text').textContent = - 'Encrypting and building PDF...'; - const doc = new PDFDocument({ - size: [pageImages[0].width, pageImages[0].height], - pdfVersion: '1.7ext3', // Use 256-bit AES encryption - userPassword: password, - ownerPassword: password, - permissions: { - printing: 'highResolution', - modifying: false, - copying: false, - annotating: false, - fillingForms: false, - contentAccessibility: true, - documentAssembly: false, - }, - }); - const stream = doc.pipe(blobStream()); - for (let i = 0; i < pageImages.length; i++) { - if (i > 0) - doc.addPage({ size: [pageImages[i].width, pageImages[i].height] }); - doc.image(pageImages[i].data, 0, 0, { - width: pageImages[i].width, - height: pageImages[i].height, - }); + if (!ownerPassword) { + args.push('--allow-insecure'); } - doc.end(); - stream.on('finish', function () { - const blob = stream.toBlob('application/pdf'); - downloadFile(blob, `encrypted-${file.name}`); - hideLoader(); - showAlert('Success', 'Encryption complete! Your download has started.'); - }); - } catch (error) { + args.push('--', outputPath); + + try { + qpdf.callMain(args); + } catch (qpdfError: any) { + console.error('qpdf execution error:', qpdfError); + throw new Error( + 'Encryption failed: ' + (qpdfError.message || 'Unknown error') + ); + } + + showLoader('Preparing download...'); + const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' }); + + if (!outputFile || outputFile.length === 0) { + throw new Error('Encryption resulted in an empty file.'); + } + + const blob = new Blob([outputFile], { type: 'application/pdf' }); + downloadFile(blob, `encrypted-${file.name}`); + + hideLoader(); + + let successMessage = 'PDF encrypted successfully with 256-bit AES!'; + if (!ownerPassword) { + successMessage += + ' Note: Without an owner password, the PDF has no usage restrictions.'; + } + + showAlert('Success', successMessage); + } catch (error: any) { console.error('Error during PDF encryption:', error); hideLoader(); - showAlert('Error', 'An error occurred. The PDF might be corrupted.'); + showAlert( + 'Encryption Failed', + `An error occurred: ${error.message || 'The PDF might be corrupted.'}` + ); + } finally { + try { + if (qpdf?.FS) { + try { + qpdf.FS.unlink(inputPath); + } catch (e) { + console.warn('Failed to unlink input file:', e); + } + try { + qpdf.FS.unlink(outputPath); + } catch (e) { + console.warn('Failed to unlink output file:', e); + } + } + } catch (cleanupError) { + console.warn('Failed to cleanup WASM FS:', cleanupError); + } } } diff --git a/src/js/ui.ts b/src/js/ui.ts index 8eff00e..ed9188a 100644 --- a/src/js/ui.ts +++ b/src/js/ui.ts @@ -379,23 +379,73 @@ export const toolTemplates = { `, encrypt: () => ` -

Encrypt PDF

-

Upload a PDF to create a new, password-protected version.

- ${createFileInputHTML()} -
-