feat: add PDF password prompt and centralized pdf-lib loader with auto-repair
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
const pageCount = result.pdf.numPages;
|
||||||
result.pdf.destroy();
|
result.pdf.destroy();
|
||||||
state.files[0] = result.file;
|
state.files[0] = result.file;
|
||||||
state.pdfDoc = await loadPdfDocument(result.bytes);
|
state.pdfDoc = await loadPdfDocument(result.bytes);
|
||||||
}
|
metaSpan.textContent = `${formatBytes(file.size)} • ${pageCount} pages`;
|
||||||
// Update page count
|
|
||||||
metaSpan.textContent = `${formatBytes(file.size)} • ${state.pdfDoc.getPageCount()} 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.');
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user