feat: add PDF password prompt and centralized pdf-lib loader with auto-repair

This commit is contained in:
alam00000
2026-03-26 14:42:48 +05:30
parent 9278774b8a
commit bd44108296
19 changed files with 89 additions and 78 deletions

View File

@@ -85,9 +85,7 @@ async function updateUI() {
} }
showLoader('Loading PDF...'); showLoader('Loading PDF...');
pageState.file = result.file; pageState.file = result.file;
pageState.pdfDoc = await loadPdfDocument(result.bytes, { pageState.pdfDoc = await loadPdfDocument(result.bytes);
throwOnInvalidObject: false,
});
result.pdf.destroy(); result.pdf.destroy();
hideLoader(); hideLoader();

View File

@@ -174,9 +174,7 @@ async function handleFiles(files: FileList) {
showLoader(translate('tools:addPageLabels.loadingPdf', 'Loading PDF...')); showLoader(translate('tools:addPageLabels.loadingPdf', 'Loading PDF...'));
try { try {
const arrayBuffer = await readFileAsArrayBuffer(file); const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await loadPdfDocument(arrayBuffer as ArrayBuffer, { const pdfDoc = await loadPdfDocument(arrayBuffer as ArrayBuffer);
throwOnInvalidObject: false,
});
if (pdfDoc.isEncrypted) { if (pdfDoc.isEncrypted) {
showAlert( showAlert(

View File

@@ -82,9 +82,7 @@ async function updateUI() {
showLoader('Loading PDF...'); showLoader('Loading PDF...');
result.pdf.destroy(); result.pdf.destroy();
pageState.file = result.file; pageState.file = result.file;
pageState.pdfDoc = await loadPdfDocument(result.bytes, { pageState.pdfDoc = await loadPdfDocument(result.bytes);
throwOnInvalidObject: false,
});
hideLoader(); hideLoader();
const pageCount = pageState.pdfDoc.getPageCount(); const pageCount = pageState.pdfDoc.getPageCount();

View File

@@ -306,9 +306,7 @@ async function performCrop() {
async function performMetadataCrop( async function performMetadataCrop(
cropData: Record<number, any> cropData: Record<number, any>
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const pdfToModify = await loadPdfDocument(cropperState.originalPdfBytes!, { const pdfToModify = await loadPdfDocument(cropperState.originalPdfBytes!);
throwOnInvalidObject: false,
});
for (const pageNum in cropData) { for (const pageNum in cropData) {
const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum)); const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
@@ -350,8 +348,7 @@ async function performFlatteningCrop(
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const sourcePdfDocForCopying = await loadPdfDocument( const sourcePdfDocForCopying = await loadPdfDocument(
cropperState.originalPdfBytes!, cropperState.originalPdfBytes!
{ throwOnInvalidObject: false }
); );
const totalPages = cropperState.pdfDoc.numPages; const totalPages = cropperState.pdfDoc.numPages;

View File

@@ -188,8 +188,7 @@ async function performFlatteningCrop(cropData: any) {
// Load the original PDF with pdf-lib to copy un-cropped pages from // Load the original PDF with pdf-lib to copy un-cropped pages from
const sourcePdfDocForCopying = await loadPdfDocument( const sourcePdfDocForCopying = await loadPdfDocument(
cropperState.originalPdfBytes, cropperState.originalPdfBytes
{ ignoreEncryption: true, throwOnInvalidObject: false }
); );
const totalPages = cropperState.pdfDoc.numPages; const totalPages = cropperState.pdfDoc.numPages;
@@ -332,8 +331,7 @@ export async function setupCropperTool() {
finalPdfBytes = await newPdfDoc.save(); finalPdfBytes = await newPdfDoc.save();
} else { } else {
const pdfToModify = await loadPdfDocument( const pdfToModify = await loadPdfDocument(
cropperState.originalPdfBytes, cropperState.originalPdfBytes
{ ignoreEncryption: true, throwOnInvalidObject: false }
); );
await performMetadataCrop(pdfToModify, finalCropData); await performMetadataCrop(pdfToModify, finalCropData);
finalPdfBytes = await pdfToModify.save(); finalPdfBytes = await pdfToModify.save();

View File

@@ -94,9 +94,7 @@ async function handleFile(file: File) {
} }
showLoader('Loading PDF...'); showLoader('Loading PDF...');
deleteState.file = result.file; deleteState.file = result.file;
deleteState.pdfDoc = await loadPdfDocument(result.bytes, { deleteState.pdfDoc = await loadPdfDocument(result.bytes);
throwOnInvalidObject: false,
});
deleteState.pdfJsDoc = result.pdf; deleteState.pdfJsDoc = result.pdf;
deleteState.totalPages = deleteState.pdfDoc.getPageCount(); deleteState.totalPages = deleteState.pdfDoc.getPageCount();
deleteState.pagesToDelete = new Set(); deleteState.pagesToDelete = new Set();

View File

@@ -229,9 +229,7 @@ async function updateUI() {
showLoader('Loading PDF...'); showLoader('Loading PDF...');
result.pdf.destroy(); result.pdf.destroy();
pageState.file = result.file; pageState.file = result.file;
pageState.pdfDoc = await loadPdfDocument(result.bytes, { pageState.pdfDoc = await loadPdfDocument(result.bytes);
throwOnInvalidObject: false,
});
hideLoader(); hideLoader();
const pageCount = pageState.pdfDoc.getPageCount(); const pageCount = pageState.pdfDoc.getPageCount();

View File

@@ -100,9 +100,7 @@ async function handleFile(file: File) {
showLoader('Loading PDF...'); showLoader('Loading PDF...');
extractState.file = result.file; extractState.file = result.file;
result.pdf.destroy(); result.pdf.destroy();
extractState.pdfDoc = await loadPdfDocument(result.bytes, { extractState.pdfDoc = await loadPdfDocument(result.bytes);
throwOnInvalidObject: false,
});
extractState.totalPages = extractState.pdfDoc.getPageCount(); extractState.totalPages = extractState.pdfDoc.getPageCount();
updateFileDisplay(); updateFileDisplay();

View File

@@ -75,9 +75,7 @@ async function updateUI() {
showLoader('Loading PDF...'); showLoader('Loading PDF...');
result.pdf.destroy(); result.pdf.destroy();
pageState.file = result.file; pageState.file = result.file;
pageState.pdfDoc = await loadPdfDocument(result.bytes, { pageState.pdfDoc = await loadPdfDocument(result.bytes);
throwOnInvalidObject: false,
});
hideLoader(); hideLoader();
const pageCount = pageState.pdfDoc.getPageCount(); const pageCount = pageState.pdfDoc.getPageCount();

View File

@@ -177,9 +177,7 @@ async function handleFile(file: File) {
if (!result) return; if (!result) return;
showLoader('Loading PDF...'); showLoader('Loading PDF...');
organizeState.pdfDoc = await loadPdfDocument(result.bytes, { organizeState.pdfDoc = await loadPdfDocument(result.bytes);
throwOnInvalidObject: false,
});
organizeState.pdfJsDoc = result.pdf; organizeState.pdfJsDoc = result.pdf;
organizeState.file = result.file; organizeState.file = result.file;
organizeState.totalPages = organizeState.pdfDoc.getPageCount(); organizeState.totalPages = organizeState.pdfDoc.getPageCount();

View File

@@ -99,9 +99,7 @@ async function updateUI() {
pageState.pdfBytes = new Uint8Array(result.bytes); pageState.pdfBytes = new Uint8Array(result.bytes);
pageState.pdfjsDoc = result.pdf; pageState.pdfjsDoc = result.pdf;
pageState.pdfDoc = await loadPdfDocument(pageState.pdfBytes, { pageState.pdfDoc = await loadPdfDocument(pageState.pdfBytes);
throwOnInvalidObject: false,
});
hideLoader(); hideLoader();

View File

@@ -436,9 +436,7 @@ async function loadPdfs(files: File[]) {
pwResult.pdf.destroy(); pwResult.pdf.destroy();
arrayBuffer = pwResult.bytes as ArrayBuffer; arrayBuffer = pwResult.bytes as ArrayBuffer;
const pdfDoc = await loadPdfDocument(arrayBuffer, { const pdfDoc = await loadPdfDocument(arrayBuffer);
throwOnInvalidObject: false,
});
currentPdfDocs.push(pdfDoc); currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1; const pdfIndex = currentPdfDocs.length - 1;
@@ -859,9 +857,7 @@ async function handleInsertPdf(e: Event) {
if (!pwResult) return; if (!pwResult) return;
pwResult.pdf.destroy(); pwResult.pdf.destroy();
const pdfDoc = await loadPdfDocument(pwResult.bytes, { const pdfDoc = await loadPdfDocument(pwResult.bytes);
throwOnInvalidObject: false,
});
currentPdfDocs.push(pdfDoc); currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1; const pdfIndex = currentPdfDocs.length - 1;

View File

@@ -236,9 +236,7 @@ async function updateUI() {
} }
showLoader('Loading PDF...'); showLoader('Loading PDF...');
pageState.pdfDoc = await loadPdfDocument(result.bytes, { pageState.pdfDoc = await loadPdfDocument(result.bytes);
throwOnInvalidObject: false,
});
pageState.pdfJsDoc = result.pdf; pageState.pdfJsDoc = result.pdf;

View File

@@ -208,9 +208,7 @@ async function updateUI() {
} }
showLoader('Loading PDF...'); showLoader('Loading PDF...');
pageState.pdfDoc = await loadPdfDocument(result.bytes, { pageState.pdfDoc = await loadPdfDocument(result.bytes);
throwOnInvalidObject: false,
});
pageState.pdfJsDoc = result.pdf; pageState.pdfJsDoc = result.pdf;

View File

@@ -90,19 +90,17 @@ document.addEventListener('DOMContentLoaded', () => {
// Load PDF Document // Load PDF Document
try { try {
if (!state.pdfDoc) { const result = await loadPdfWithPasswordPrompt(file);
const result = await loadPdfWithPasswordPrompt(file); if (!result) {
if (!result) { state.files = [];
state.files = []; updateUI();
updateUI(); return;
return;
}
result.pdf.destroy();
state.files[0] = result.file;
state.pdfDoc = await loadPdfDocument(result.bytes);
} }
// Update page count const pageCount = result.pdf.numPages;
metaSpan.textContent = `${formatBytes(file.size)}${state.pdfDoc.getPageCount()} pages`; result.pdf.destroy();
state.files[0] = result.file;
state.pdfDoc = await loadPdfDocument(result.bytes);
metaSpan.textContent = `${formatBytes(file.size)}${pageCount} pages`;
} catch (error) { } catch (error) {
console.error('Error loading PDF:', error); console.error('Error loading PDF:', error);
showAlert('Error', 'Failed to load PDF file.'); showAlert('Error', 'Failed to load PDF file.');

View File

@@ -1,14 +1,68 @@
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import { initializeQpdf } from './helpers.js';
type LoadOptions = Parameters<typeof PDFDocument.load>[1]; type LoadOptions = Parameters<typeof PDFDocument.load>[1];
type PDFDocumentInstance = Awaited<ReturnType<typeof PDFDocument.load>>; type PDFDocumentInstance = Awaited<ReturnType<typeof PDFDocument.load>>;
async function repairPdfBytes(pdf: Uint8Array): Promise<Uint8Array | null> {
try {
const qpdf = await initializeQpdf();
qpdf.FS.writeFile('/input.pdf', pdf);
try {
qpdf.callMain(['/input.pdf', '--decrypt', '/output.pdf']);
} catch (e) {
console.warn('[loadPdfDocument] qpdf repair warning:', e);
}
let repaired: Uint8Array | null = null;
try {
repaired = qpdf.FS.readFile('/output.pdf', { encoding: 'binary' });
} catch (e) {
console.warn('[loadPdfDocument] Failed to read repaired output:', e);
}
try {
qpdf.FS.unlink('/input.pdf');
} catch (e) {
console.warn('[loadPdfDocument] Cleanup error:', e);
}
try {
qpdf.FS.unlink('/output.pdf');
} catch (e) {
console.warn('[loadPdfDocument] Cleanup error:', e);
}
return repaired;
} catch (e) {
console.warn('[loadPdfDocument] qpdf not available for repair:', e);
return null;
}
}
export async function loadPdfDocument( export async function loadPdfDocument(
pdf: Uint8Array | ArrayBuffer, pdf: Uint8Array | ArrayBuffer,
options?: LoadOptions options?: LoadOptions
): Promise<PDFDocumentInstance> { ): Promise<PDFDocumentInstance> {
return PDFDocument.load(pdf, { const loadOpts = {
ignoreEncryption: true, ignoreEncryption: true,
throwOnInvalidObject: false,
...options, ...options,
}); };
const inputBytes = pdf instanceof Uint8Array ? pdf : new Uint8Array(pdf);
const repaired = await repairPdfBytes(inputBytes);
if (repaired) {
try {
return await PDFDocument.load(repaired, loadOpts);
} catch (e) {
console.warn(
'[loadPdfDocument] Failed to load repaired PDF, falling back to original:',
e
);
}
}
return PDFDocument.load(pdf, loadOpts);
} }

View File

@@ -37,9 +37,7 @@ export class DecryptNode extends BaseWorkflowNode {
input.bytes, input.bytes,
password password
); );
const document = await loadPdfDocument(resultBytes, { const document = await loadPdfDocument(resultBytes);
throwOnInvalidObject: false,
});
return { return {
type: 'pdf', type: 'pdf',

View File

@@ -31,16 +31,14 @@ export class PDFInputNode extends BaseWorkflowNode {
let isEncrypted = false; let isEncrypted = false;
try { try {
await loadPdfDocument(bytes, { throwOnInvalidObject: false }); await loadPdfDocument(bytes);
} catch { } catch {
isEncrypted = true; isEncrypted = true;
} }
if (isEncrypted) { if (isEncrypted) {
try { try {
await loadPdfDocument(bytes, { await loadPdfDocument(bytes);
throwOnInvalidObject: false,
});
} catch { } catch {
throw new Error( throw new Error(
`Failed to load "${file.name}" - file may be corrupted` `Failed to load "${file.name}" - file may be corrupted`
@@ -49,9 +47,7 @@ export class PDFInputNode extends BaseWorkflowNode {
throw new EncryptedPDFError(file.name); throw new EncryptedPDFError(file.name);
} }
const document = await loadPdfDocument(bytes, { const document = await loadPdfDocument(bytes);
throwOnInvalidObject: false,
});
this.files.push({ this.files.push({
type: 'pdf', type: 'pdf',
document, document,
@@ -64,9 +60,7 @@ export class PDFInputNode extends BaseWorkflowNode {
const arrayBuffer = await readFileAsArrayBuffer(file); const arrayBuffer = await readFileAsArrayBuffer(file);
const bytes = new Uint8Array(arrayBuffer as ArrayBuffer); const bytes = new Uint8Array(arrayBuffer as ArrayBuffer);
const { bytes: decryptedBytes } = await decryptPdfBytes(bytes, password); const { bytes: decryptedBytes } = await decryptPdfBytes(bytes, password);
const document = await loadPdfDocument(decryptedBytes, { const document = await loadPdfDocument(decryptedBytes);
throwOnInvalidObject: false,
});
this.files.push({ this.files.push({
type: 'pdf', type: 'pdf',
document, document,

View File

@@ -49,9 +49,7 @@ export class RepairNode extends BaseWorkflowNode {
} }
const resultBytes = new Uint8Array(repairedData); const resultBytes = new Uint8Array(repairedData);
const resultDoc = await loadPdfDocument(resultBytes, { const resultDoc = await loadPdfDocument(resultBytes);
throwOnInvalidObject: false,
});
return { return {
type: 'pdf', type: 'pdf',