feat(permissions): rewrite pdf permission handling using qpdf-wasm

- Replace PDFKit with qpdf-wasm for more reliable encryption and permission handling
- Improve error handling for password-related cases
- Update UI text and styling for better clarity
- Add support for additional permission controls (page extraction)
- Optimize performance by removing page-by-page rendering
This commit is contained in:
abdullahalam123
2025-10-24 00:47:39 +05:30
parent 466646d15b
commit e0d3075609
2 changed files with 198 additions and 143 deletions

View File

@@ -1,146 +1,180 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import PDFDocument from 'pdfkit/js/pdfkit.standalone'; import createModule from '@neslinesli93/qpdf-wasm';
import blobStream from 'blob-stream';
import * as pdfjsLib from 'pdfjs-dist'; let qpdfInstance: any = null;
async function initializeQpdf() {
if (qpdfInstance) {
return qpdfInstance;
}
showLoader('Initializing PDF 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 PDF engine. Please refresh the page and try again.'
);
throw error;
} finally {
hideLoader();
}
return qpdfInstance;
}
export async function changePermissions() { export async function changePermissions() {
const file = state.files[0];
const currentPassword = ( const currentPassword = (
document.getElementById('current-password') as HTMLInputElement document.getElementById('current-password') as HTMLInputElement
).value; )?.value || '';
const newUserPassword = ( const newUserPassword = (
document.getElementById('new-user-password') as HTMLInputElement document.getElementById('new-user-password') as HTMLInputElement
).value; )?.value || '';
const newOwnerPassword = ( const newOwnerPassword = (
document.getElementById('new-owner-password') as HTMLInputElement document.getElementById('new-owner-password') as HTMLInputElement
).value; )?.value || '';
// An owner password is required to enforce any permissions. const inputPath = '/input.pdf';
if ( const outputPath = '/output.pdf';
!newOwnerPassword && let qpdf: any;
(newUserPassword ||
document.querySelectorAll('input[type="checkbox"]:not(:checked)').length >
0)
) {
showAlert(
'Input Required',
'You must set a "New Owner Password" to enforce specific permissions or to set a user password.'
);
return;
}
showLoader('Preparing to process...');
try { try {
const file = state.files[0]; showLoader('Initializing...');
const pdfData = await readFileAsArrayBuffer(file); qpdf = await initializeQpdf();
let pdf; showLoader('Reading PDF...');
try { const fileBuffer = await readFileAsArrayBuffer(file);
pdf = await pdfjsLib.getDocument({ const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
data: pdfData as ArrayBuffer, qpdf.FS.writeFile(inputPath, uint8Array);
password: currentPassword,
}).promise; showLoader('Processing PDF permissions...');
} catch (e) {
// This catch is specific to password errors in pdf.js const args = [inputPath];
if (e.name === 'PasswordException') {
hideLoader(); // Add password if provided
showAlert( if (currentPassword) {
'Incorrect Password', args.push('--password=' + currentPassword);
'The current password you entered is incorrect.' }
);
return; const shouldEncrypt = newUserPassword || newOwnerPassword;
if (shouldEncrypt) {
const finalUserPassword = newUserPassword;
const finalOwnerPassword = newOwnerPassword;
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
// Get permission checkboxes
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement)?.checked;
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement)?.checked;
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement)?.checked;
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement)?.checked;
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement)?.checked;
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement)?.checked;
const allowPageExtraction = (document.getElementById('allow-page-extraction') as HTMLInputElement)?.checked;
if (finalOwnerPassword) {
if (!allowModifying) args.push('--modify=none');
if (!allowCopying) args.push('--extract=n');
if (!allowPrinting) args.push('--print=none');
if (!allowAnnotating) args.push('--annotate=n');
if (!allowDocumentAssembly) args.push('--assemble=n');
if (!allowFillingForms) args.push('--form=n');
if (!allowPageExtraction) args.push('--extract=n');
// --modify-other is not directly mapped, apply if modifying is disabled
if (!allowModifying) args.push('--modify-other=n');
} else if (finalUserPassword) {
args.push('--allow-insecure');
} }
throw e; } else {
args.push('--decrypt');
} }
const numPages = pdf.numPages; args.push('--', outputPath);
const pageImages = [];
for (let i = 1; i <= numPages; i++) { console.log('qpdf args:', args);
document.getElementById('loader-text').textContent =
`Processing page ${i} of ${numPages}...`; try {
const page = await pdf.getPage(i); qpdf.callMain(args);
const viewport = page.getViewport({ scale: 2.0 }); } catch (qpdfError: any) {
const canvas = document.createElement('canvas'); console.error('qpdf execution error:', qpdfError);
canvas.height = viewport.height;
canvas.width = viewport.width; // Check for various password-related errors
const context = canvas.getContext('2d'); const errorMsg = qpdfError.message || '';
await page.render({ canvasContext: context, viewport: viewport }).promise;
pageImages.push({ if (errorMsg.includes('invalid password') ||
data: canvas.toDataURL('image/jpeg', 0.8), errorMsg.includes('incorrect password') ||
width: viewport.width, errorMsg.includes('password')) {
height: viewport.height, throw new Error('INVALID_PASSWORD');
}); }
if (errorMsg.includes('encrypted') ||
errorMsg.includes('password required')) {
throw new Error('PASSWORD_REQUIRED');
}
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
} }
document.getElementById('loader-text').textContent = showLoader('Preparing download...');
'Applying new permissions...'; const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
const allowPrinting = ( if (!outputFile || outputFile.length === 0) {
document.getElementById('allow-printing') as HTMLInputElement throw new Error('Processing resulted in an empty file.');
).checked;
const allowCopying = (
document.getElementById('allow-copying') as HTMLInputElement
).checked;
const allowModifying = (
document.getElementById('allow-modifying') as HTMLInputElement
).checked;
const allowAnnotating = (
document.getElementById('allow-annotating') as HTMLInputElement
).checked;
const allowFillingForms = (
document.getElementById('allow-filling-forms') as HTMLInputElement
).checked;
const allowContentAccessibility = (
document.getElementById('allow-content-accessibility') as HTMLInputElement
).checked;
const allowDocumentAssembly = (
document.getElementById('allow-document-assembly') as HTMLInputElement
).checked;
const doc = new PDFDocument({
size: [pageImages[0].width, pageImages[0].height],
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
// Apply the new, separate user and owner passwords
userPassword: newUserPassword,
ownerPassword: newOwnerPassword,
// Apply all seven permissions from the checkboxes
permissions: {
printing: allowPrinting ? 'highResolution' : false,
modifying: allowModifying,
copying: allowCopying,
annotating: allowAnnotating,
fillingForms: allowFillingForms,
contentAccessibility: allowContentAccessibility,
documentAssembly: allowDocumentAssembly,
},
});
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(); const blob = new Blob([outputFile], { type: 'application/pdf' });
stream.on('finish', function () { downloadFile(blob, `permissions-changed-${file.name}`);
const blob = stream.toBlob('application/pdf');
downloadFile(blob, `permissions-changed-${file.name}`);
hideLoader();
showAlert('Success', 'Permissions changed successfully!');
});
} catch (e) {
console.error(e);
hideLoader(); hideLoader();
showAlert('Error', `An unexpected error occurred: ${e.message}`);
let successMessage = 'PDF permissions changed successfully!';
if (!shouldEncrypt) {
successMessage = 'PDF decrypted successfully! All encryption and restrictions removed.';
}
showAlert('Success', successMessage);
} catch (error: any) {
console.error('Error during PDF permission change:', error);
hideLoader();
if (error.message === 'INVALID_PASSWORD') {
showAlert(
'Incorrect Password',
'The current password you entered is incorrect. Please try again.'
);
} else if (error.message === 'PASSWORD_REQUIRED') {
showAlert(
'Password Required',
'This PDF is password-protected. Please enter the current password to proceed.'
);
} else {
showAlert(
'Processing Failed',
`An error occurred: ${error.message || 'The PDF might be corrupted.'}`
);
}
} finally {
try {
if (qpdf?.FS) {
try {
qpdf.FS.unlink(inputPath);
} catch (e) {
}
try {
qpdf.FS.unlink(outputPath);
} catch (e) {
}
}
} catch (cleanupError) {
console.warn('Failed to cleanup WASM FS:', cleanupError);
}
} }
} }

View File

@@ -913,52 +913,73 @@ export const toolTemplates = {
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button> <button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`, `,
'change-permissions': () => ` 'change-permissions': () => `
<h2 class="text-2xl font-bold text-white mb-4">Change PDF Permissions</h2> <h2 class="text-2xl font-bold text-white mb-4">Change PDF Permissions</h2>
<p class="mb-6 text-gray-400">Unlock a PDF and re-save it with new passwords and permissions.</p> <p class="mb-6 text-gray-400">Modify passwords and permissions without losing quality.</p>
${createFileInputHTML()} ${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="permissions-options" class="hidden mt-6 space-y-4"> <div id="permissions-options" class="hidden mt-6 space-y-4">
<div> <div>
<label for="current-password" class="block mb-2 text-sm font-medium text-gray-300">Current Password (to unlock)</label> <label for="current-password" class="block mb-2 text-sm font-medium text-gray-300">Current Password (if encrypted)</label>
<input type="password" id="current-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Enter current password to open file"> <input type="password" id="current-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Leave blank if PDF is not password-protected">
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label for="new-user-password" class="block mb-2 text-sm font-medium text-gray-300">New User Password (optional)</label> <label for="new-user-password" class="block mb-2 text-sm font-medium text-gray-300">New User Password (optional)</label>
<input type="password" id="new-user-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password to open file"> <input type="password" id="new-user-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password to open PDF">
</div> </div>
<div> <div>
<label for="new-owner-password" class="block mb-2 text-sm font-medium text-gray-300">New Owner Password (optional)</label> <label for="new-owner-password" class="block mb-2 text-sm font-medium text-gray-300">New Owner Password (optional)</label>
<input type="password" id="new-owner-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Admin password for permissions"> <input type="password" id="new-owner-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password for full permissions">
</div> </div>
</div> </div>
<div class="p-4 bg-gray-900 border border-blue-500/30 text-blue-200 rounded-lg"> <div class="p-4 bg-blue-900/20 border border-blue-500/30 text-blue-200 rounded-lg">
<h3 class="font-semibold text-base mb-2">How Passwords Work</h3> <h3 class="font-semibold text-base mb-2">📝 How It Works</h3>
<ul class="list-disc list-inside text-sm text-gray-400 space-y-1"> <ul class="list-disc list-inside text-sm text-gray-300 space-y-1">
<li>The <strong>User Password</strong> is required to open and decrypt the PDF.</li> <li><strong>User Password:</strong> Required to open the PDF</li>
<li>The <strong>Owner Password</strong> is an admin key that bypasses all permissions.</li> <li><strong>Owner Password:</strong> Required to enforce the permissions below</li>
<li>Leave both blank to create an unprotected PDF.</li> <li>Leave both blank to remove all encryption and restrictions</li>
<li>Set an Owner Password to enforce the permissions you select below.</li> <li>Check boxes below to ALLOW specific actions (unchecked = disabled)</li>
</ul> </ul>
</div> </div>
<fieldset class="border border-gray-600 p-4 rounded-lg"> <fieldset class="border border-gray-600 p-4 rounded-lg">
<legend class="px-2 text-sm font-medium text-gray-300">Allow User to:</legend> <legend class="px-2 text-sm font-medium text-gray-300">Permissions (only enforced with Owner Password):</legend>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2"> <div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
<label class="flex items-center gap-2"><input type="checkbox" id="allow-printing" checked class="checkbox-style"> Print Document</label> <label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
<label class="flex items-center gap-2"><input type="checkbox" id="allow-copying" checked class="checkbox-style"> Copy Content</label> <input type="checkbox" id="allow-printing" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
<label class="flex items-center gap-2"><input type="checkbox" id="allow-modifying" checked class="checkbox-style"> Modify Document</label> Allow Printing
<label class="flex items-center gap-2"><input type="checkbox" id="allow-annotating" checked class="checkbox-style"> Annotate & Comment</label> </label>
<label class="flex items-center gap-2"><input type="checkbox" id="allow-filling-forms" checked class="checkbox-style"> Fill in Forms</label> <label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
<label class="flex items-center gap-2"><input type="checkbox" id="allow-content-accessibility" checked class="checkbox-style"> Enable Content Accessibility</label> <input type="checkbox" id="allow-copying" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
<label class="flex items-center gap-2"><input type="checkbox" id="allow-document-assembly" checked class="checkbox-style"> Assemble Document</label> Allow Text/Image Extraction
</label>
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
<input type="checkbox" id="allow-modifying" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
Allow Modifications
</label>
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
<input type="checkbox" id="allow-annotating" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
Allow Annotations
</label>
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
<input type="checkbox" id="allow-filling-forms" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
Allow Form Filling
</label>
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
<input type="checkbox" id="allow-document-assembly" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
Allow Page Assembly
</label>
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
<input type="checkbox" id="allow-page-extraction" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
Allow Page Extraction
</label>
</div> </div>
</fieldset> </fieldset>
</div> </div>
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Save New Permissions</button> <button id="process-btn" class="hidden btn-gradient w-full mt-6">Apply Changes</button>
`, `,
'pdf-to-markdown': () => ` 'pdf-to-markdown': () => `