From 85ce0c97b52c9e60f36376d7abb19a768d41752b Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Tue, 11 Nov 2025 11:13:06 +0530 Subject: [PATCH 1/2] feat: implement PDF attachment extraction functionality with web worker support - Added a new worker script to handle extraction of embedded attachments from PDF files. - Created TypeScript definitions for the message structure and response types. - Updated the main extraction logic to utilize the worker for improved performance and responsiveness. - Integrated the extraction feature into the UI, allowing users to extract attachments as a ZIP file. - Enhanced error handling and user feedback during the extraction process. --- .../workers/extract-attachments.worker.d.ts | 19 ++ public/workers/extract-attachments.worker.js | 106 ++++++++++ src/js/config/tools.ts | 13 +- src/js/logic/extract-attachments.ts | 198 +++++++++++------- src/js/logic/index.ts | 4 +- 5 files changed, 253 insertions(+), 87 deletions(-) create mode 100644 public/workers/extract-attachments.worker.d.ts create mode 100644 public/workers/extract-attachments.worker.js diff --git a/public/workers/extract-attachments.worker.d.ts b/public/workers/extract-attachments.worker.d.ts new file mode 100644 index 0000000..2bf1a2d --- /dev/null +++ b/public/workers/extract-attachments.worker.d.ts @@ -0,0 +1,19 @@ +declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf; + +interface ExtractAttachmentsMessage { + command: 'extract-attachments'; + fileBuffers: ArrayBuffer[]; + fileNames: string[]; +} + +interface ExtractAttachmentSuccessResponse { + status: 'success'; + attachments: Array<{ name: string; data: ArrayBuffer }>; +} + +interface ExtractAttachmentErrorResponse { + status: 'error'; + message: string; +} + +type ExtractAttachmentResponse = ExtractAttachmentSuccessResponse | ExtractAttachmentErrorResponse; \ No newline at end of file diff --git a/public/workers/extract-attachments.worker.js b/public/workers/extract-attachments.worker.js new file mode 100644 index 0000000..0327c3a --- /dev/null +++ b/public/workers/extract-attachments.worker.js @@ -0,0 +1,106 @@ +self.importScripts('/coherentpdf.browser.min.js'); + +function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) { + try { + const allAttachments = []; + const totalFiles = fileBuffers.length; + + for (let i = 0; i < totalFiles; i++) { + const buffer = fileBuffers[i]; + const fileName = fileNames[i]; + const uint8Array = new Uint8Array(buffer); + + let pdf; + try { + pdf = coherentpdf.fromMemory(uint8Array, ''); + } catch (error) { + console.warn(`Failed to load PDF: ${fileName}`, error); + continue; + } + + coherentpdf.startGetAttachments(pdf); + const attachmentCount = coherentpdf.numberGetAttachments(); + + if (attachmentCount === 0) { + console.warn(`No attachments found in ${fileName}`); + coherentpdf.deletePdf(pdf); + continue; + } + + const baseName = fileName.replace(/\.pdf$/i, ''); + for (let j = 0; j < attachmentCount; j++) { + try { + const attachmentName = coherentpdf.getAttachmentName(j); + const attachmentPage = coherentpdf.getAttachmentPage(j); + const attachmentData = coherentpdf.getAttachmentData(j); + + let uniqueName = attachmentName; + let counter = 1; + while (allAttachments.some(att => att.name === uniqueName)) { + const nameParts = attachmentName.split('.'); + if (nameParts.length > 1) { + const extension = nameParts.pop(); + uniqueName = `${nameParts.join('.')}_${counter}.${extension}`; + } else { + uniqueName = `${attachmentName}_${counter}`; + } + counter++; + } + + if (attachmentPage > 0) { + uniqueName = `${baseName}_page${attachmentPage}_${uniqueName}`; + } else { + uniqueName = `${baseName}_${uniqueName}`; + } + + allAttachments.push({ + name: uniqueName, + data: attachmentData.buffer.slice(0) + }); + } catch (error) { + console.warn(`Failed to extract attachment ${j} from ${fileName}:`, error); + } + } + + coherentpdf.endGetAttachments(); + coherentpdf.deletePdf(pdf); + } + + if (allAttachments.length === 0) { + self.postMessage({ + status: 'error', + message: 'No attachments were found in the selected PDF(s).' + }); + return; + } + + const response = { + status: 'success', + attachments: [] + }; + + const transferBuffers = []; + for (const attachment of allAttachments) { + response.attachments.push({ + name: attachment.name, + data: attachment.data + }); + transferBuffers.push(attachment.data); + } + + self.postMessage(response, transferBuffers); + } catch (error) { + self.postMessage({ + status: 'error', + message: error instanceof Error + ? error.message + : 'Unknown error occurred during attachment extraction.' + }); + } +} + +self.onmessage = (e) => { + if (e.data.command === 'extract-attachments') { + extractAttachmentsFromPDFsInWorker(e.data.fileBuffers, e.data.fileNames); + } +}; \ No newline at end of file diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 1931f9e..5e61627 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -318,13 +318,12 @@ export const categories = [ icon: 'paperclip', subtitle: 'Embed one or more files into your PDF.', }, - // TODO@ALAM - MAKE THIS LATER, ONCE INTEGERATED WITH CPDF - // { - // id: 'extract-attachments', - // name: 'Extract Attachments', - // icon: 'download', - // subtitle: 'Extract all embedded files from PDF(s) as a ZIP.', - // }, + { + id: 'extract-attachments', + name: 'Extract Attachments', + icon: 'download', + subtitle: 'Extract all embedded files from PDF(s) as a ZIP.', + }, // { // id: 'edit-attachments', // name: 'Edit Attachments', diff --git a/src/js/logic/extract-attachments.ts b/src/js/logic/extract-attachments.ts index df7c81f..035b2c3 100644 --- a/src/js/logic/extract-attachments.ts +++ b/src/js/logic/extract-attachments.ts @@ -1,88 +1,130 @@ -// TODO@ALAM - USE CPDF HERE +import { downloadFile, formatBytes } from '../utils/helpers.js'; +import { state } from '../state.js'; +import JSZip from 'jszip'; -// import { showLoader, hideLoader, showAlert } from '../ui.js'; -// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; -// import { state } from '../state.js'; -// import { PDFDocument as PDFLibDocument } from 'pdf-lib'; -// import JSZip from 'jszip'; +const worker = new Worker('/workers/extract-attachments.worker.js'); -// export async function extractAttachments() { -// if (state.files.length === 0) { -// showAlert('No Files', 'Please select at least one PDF file.'); -// return; -// } +interface ExtractAttachmentSuccessResponse { + status: 'success'; + attachments: Array<{ name: string; data: ArrayBuffer }>; +} -// showLoader('Extracting attachments...'); -// try { -// const zip = new JSZip(); -// let totalAttachments = 0; +interface ExtractAttachmentErrorResponse { + status: 'error'; + message: string; +} -// for (const file of state.files) { -// const pdfBytes = await readFileAsArrayBuffer(file); -// const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { -// ignoreEncryption: true, -// }); +type ExtractAttachmentResponse = ExtractAttachmentSuccessResponse | ExtractAttachmentErrorResponse; -// const embeddedFiles = pdfDoc.context.enumerateIndirectObjects() -// .filter(([ref, obj]: any) => { -// // obj must be a PDFDict -// if (obj && typeof obj.get === 'function') { -// const type = obj.get('Type'); -// return type && type.toString() === '/Filespec'; -// } -// return false; -// }); +export async function extractAttachments() { + if (state.files.length === 0) { + showStatus('No Files', 'error'); + return; + } -// if (embeddedFiles.length === 0) { -// console.warn(`No attachments found in ${file.name}`); -// continue; -// } + document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed'); + document.getElementById('process-btn')?.setAttribute('disabled', 'true'); + + showStatus('Reading files (Main Thread)...', 'info'); -// // Extract attachments -// const baseName = file.name.replace(/\.pdf$/i, ''); -// for (let i = 0; i < embeddedFiles.length; i++) { -// try { -// const [ref, fileSpec] = embeddedFiles[i]; -// const fileSpecDict = fileSpec as any; - -// // Get attachment name -// const fileName = fileSpecDict.get('UF')?.decodeText() || -// fileSpecDict.get('F')?.decodeText() || -// `attachment-${i + 1}`; - -// // Get embedded file stream -// const ef = fileSpecDict.get('EF'); -// if (ef) { -// const fRef = ef.get('F') || ef.get('UF'); -// if (fRef) { -// const fileStream = pdfDoc.context.lookup(fRef); -// if (fileStream) { -// const fileData = (fileStream as any).getContents(); -// zip.file(`${baseName}_${fileName}`, fileData); -// totalAttachments++; -// } -// } -// } -// } catch (e) { -// console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e); -// } -// } -// } + try { + const fileBuffers: ArrayBuffer[] = []; + const fileNames: string[] = []; -// if (totalAttachments === 0) { -// showAlert('No Attachments', 'No attachments were found in the selected PDF(s).'); -// hideLoader(); -// return; -// } + for (const file of state.files) { + const buffer = await file.arrayBuffer(); + fileBuffers.push(buffer); + fileNames.push(file.name); + } -// const zipBlob = await zip.generateAsync({ type: 'blob' }); -// downloadFile(zipBlob, 'extracted-attachments.zip'); -// showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`); -// } catch (e) { -// console.error(e); -// showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.'); -// } finally { -// hideLoader(); -// } -// } + showStatus(`Extracting attachments from ${state.files.length} file(s)...`, 'info'); + const message: ExtractAttachmentsMessage = { + command: 'extract-attachments', + fileBuffers, + fileNames, + }; + + const transferables = fileBuffers.map(buf => buf); + worker.postMessage(message, transferables); + + } catch (error) { + console.error('Error reading files:', error); + showStatus( + `Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`, + 'error' + ); + document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed'); + document.getElementById('process-btn')?.removeAttribute('disabled'); + } +} + +worker.onmessage = (e: MessageEvent) => { + document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed'); + document.getElementById('process-btn')?.removeAttribute('disabled'); + + if (e.data.status === 'success') { + const attachments = e.data.attachments; + + const zip = new JSZip(); + let totalSize = 0; + + for (const attachment of attachments) { + zip.file(attachment.name, new Uint8Array(attachment.data)); + totalSize += attachment.data.byteLength; + } + + zip.generateAsync({ type: 'blob' }).then((zipBlob) => { + downloadFile(zipBlob, 'extracted-attachments.zip'); + showStatus( + `Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`, + 'success' + ); + + state.files = []; + const fileDisplayArea = document.getElementById('file-display-area'); + if (fileDisplayArea) { + fileDisplayArea.innerHTML = ''; + fileDisplayArea.classList.add('hidden'); + } + const fileInput = document.getElementById('file-input') as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed'); + document.getElementById('process-btn')?.setAttribute('disabled', 'true'); + }); + } else if (e.data.status === 'error') { + const errorMessage = e.data.message || 'Unknown error occurred in worker.'; + console.error('Worker Error:', errorMessage); + showStatus(`Error: ${errorMessage}`, 'error'); + } +}; + +worker.onerror = (error) => { + console.error('Worker error:', error); + showStatus('Worker error occurred. Check console for details.', 'error'); + document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed'); + document.getElementById('process-btn')?.removeAttribute('disabled'); +}; + +function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') { + const statusMessage = document.getElementById('status-message') as HTMLElement; + if (!statusMessage) return; + + statusMessage.textContent = message; + statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${ + type === 'success' + ? 'bg-green-900 text-green-200' + : type === 'error' + ? 'bg-red-900 text-red-200' + : 'bg-blue-900 text-blue-200' + }`; + statusMessage.classList.remove('hidden'); +} + +interface ExtractAttachmentsMessage { + command: 'extract-attachments'; + fileBuffers: ArrayBuffer[]; + fileNames: string[]; +} \ No newline at end of file diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index 780d450..1b50560 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -63,7 +63,7 @@ import { import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js'; import { linearizePdf } from './linearize.js'; import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js'; -// import { extractAttachments } from './extract-attachments.js'; +import { extractAttachments } from './extract-attachments.js'; // import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js'; import { sanitizePdf } from './sanitize-pdf.js'; import { removeRestrictions } from './remove-restrictions.js'; @@ -140,7 +140,7 @@ export const toolLogic = { process: addAttachments, setup: setupAddAttachmentsTool, }, - // 'extract-attachments': extractAttachments, + 'extract-attachments': extractAttachments, // 'edit-attachments': { // process: editAttachments, // setup: setupEditAttachmentsTool, From 842d94080172ca342e471bb89d5dd81a9e223d30 Mon Sep 17 00:00:00 2001 From: abdullahalam123 Date: Tue, 11 Nov 2025 12:21:11 +0530 Subject: [PATCH 2/2] feat: implement PDF attachment editing functionality with web worker support - Added a new worker script to handle editing of embedded attachments in PDF files. - Created TypeScript definitions for message structures and response types related to attachment editing. - Updated the main logic to utilize the worker for improved performance and responsiveness during attachment management. - Integrated the editing feature into the UI, allowing users to view, remove, or replace attachments in their PDFs. - Enhanced error handling and user feedback during the editing process. --- public/workers/edit-attachments.worker.d.ts | 42 +++ public/workers/edit-attachments.worker.js | 151 ++++++++ src/js/config/tools.ts | 12 +- src/js/logic/edit-attachments.ts | 379 ++++++++++---------- src/js/logic/index.ts | 10 +- 5 files changed, 399 insertions(+), 195 deletions(-) create mode 100644 public/workers/edit-attachments.worker.d.ts create mode 100644 public/workers/edit-attachments.worker.js diff --git a/public/workers/edit-attachments.worker.d.ts b/public/workers/edit-attachments.worker.d.ts new file mode 100644 index 0000000..fdca01a --- /dev/null +++ b/public/workers/edit-attachments.worker.d.ts @@ -0,0 +1,42 @@ +declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf; + +interface GetAttachmentsMessage { + command: 'get-attachments'; + fileBuffer: ArrayBuffer; + fileName: string; +} + +interface EditAttachmentsMessage { + command: 'edit-attachments'; + fileBuffer: ArrayBuffer; + fileName: string; + attachmentsToRemove: number[]; +} + +type EditAttachmentsWorkerMessage = GetAttachmentsMessage | EditAttachmentsMessage; + +interface GetAttachmentsSuccessResponse { + status: 'success'; + attachments: Array<{ index: number; name: string; page: number; data: ArrayBuffer }>; + fileName: string; +} + +interface GetAttachmentsErrorResponse { + status: 'error'; + message: string; +} + +interface EditAttachmentsSuccessResponse { + status: 'success'; + modifiedPDF: ArrayBuffer; + fileName: string; +} + +interface EditAttachmentsErrorResponse { + status: 'error'; + message: string; +} + +type GetAttachmentsResponse = GetAttachmentsSuccessResponse | GetAttachmentsErrorResponse; +type EditAttachmentsResponse = EditAttachmentsSuccessResponse | EditAttachmentsErrorResponse; +type EditAttachmentsWorkerResponse = GetAttachmentsResponse | EditAttachmentsResponse; \ No newline at end of file diff --git a/public/workers/edit-attachments.worker.js b/public/workers/edit-attachments.worker.js new file mode 100644 index 0000000..4d20160 --- /dev/null +++ b/public/workers/edit-attachments.worker.js @@ -0,0 +1,151 @@ +self.importScripts('/coherentpdf.browser.min.js'); + +function getAttachmentsFromPDFInWorker(fileBuffer, fileName) { + try { + const uint8Array = new Uint8Array(fileBuffer); + + let pdf; + try { + pdf = coherentpdf.fromMemory(uint8Array, ''); + } catch (error) { + self.postMessage({ + status: 'error', + message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}` + }); + return; + } + + coherentpdf.startGetAttachments(pdf); + const attachmentCount = coherentpdf.numberGetAttachments(); + + if (attachmentCount === 0) { + self.postMessage({ + status: 'success', + attachments: [], + fileName: fileName + }); + coherentpdf.deletePdf(pdf); + return; + } + + const attachments = []; + for (let i = 0; i < attachmentCount; i++) { + try { + const name = coherentpdf.getAttachmentName(i); + const page = coherentpdf.getAttachmentPage(i); + const attachmentData = coherentpdf.getAttachmentData(i); + + const dataArray = new Uint8Array(attachmentData); + const buffer = dataArray.buffer.slice(dataArray.byteOffset, dataArray.byteOffset + dataArray.byteLength); + + attachments.push({ + index: i, + name: String(name), + page: Number(page), + data: buffer + }); + } catch (error) { + console.warn(`Failed to get attachment ${i} from ${fileName}:`, error); + } + } + + coherentpdf.endGetAttachments(); + coherentpdf.deletePdf(pdf); + + const response = { + status: 'success', + attachments: attachments, + fileName: fileName + }; + + const transferBuffers = attachments.map(att => att.data); + self.postMessage(response, transferBuffers); + } catch (error) { + self.postMessage({ + status: 'error', + message: error instanceof Error + ? error.message + : 'Unknown error occurred during attachment listing.' + }); + } +} + +function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove) { + try { + const uint8Array = new Uint8Array(fileBuffer); + + let pdf; + try { + pdf = coherentpdf.fromMemory(uint8Array, ''); + } catch (error) { + self.postMessage({ + status: 'error', + message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}` + }); + return; + } + + if (attachmentsToRemove && attachmentsToRemove.length > 0) { + coherentpdf.startGetAttachments(pdf); + const attachmentCount = coherentpdf.numberGetAttachments(); + const attachmentsToKeep = []; + + for (let i = 0; i < attachmentCount; i++) { + if (!attachmentsToRemove.includes(i)) { + const name = coherentpdf.getAttachmentName(i); + const page = coherentpdf.getAttachmentPage(i); + const data = coherentpdf.getAttachmentData(i); + + const dataCopy = new Uint8Array(data.length); + dataCopy.set(new Uint8Array(data)); + + attachmentsToKeep.push({ + name: String(name), + page: Number(page), + data: dataCopy + }); + } + } + + coherentpdf.endGetAttachments(); + + coherentpdf.removeAttachedFiles(pdf); + + for (const attachment of attachmentsToKeep) { + if (attachment.page === 0) { + coherentpdf.attachFileFromMemory(attachment.data, attachment.name, pdf); + } else { + coherentpdf.attachFileToPageFromMemory(attachment.data, attachment.name, pdf, attachment.page); + } + } + } + + const modifiedBytes = coherentpdf.toMemory(pdf, false, true); + coherentpdf.deletePdf(pdf); + + const buffer = modifiedBytes.buffer.slice(modifiedBytes.byteOffset, modifiedBytes.byteOffset + modifiedBytes.byteLength); + + const response = { + status: 'success', + modifiedPDF: buffer, + fileName: fileName + }; + + self.postMessage(response, [response.modifiedPDF]); + } catch (error) { + self.postMessage({ + status: 'error', + message: error instanceof Error + ? error.message + : 'Unknown error occurred during attachment editing.' + }); + } +} + +self.onmessage = (e) => { + if (e.data.command === 'get-attachments') { + getAttachmentsFromPDFInWorker(e.data.fileBuffer, e.data.fileName); + } else if (e.data.command === 'edit-attachments') { + editAttachmentsInPDFInWorker(e.data.fileBuffer, e.data.fileName, e.data.attachmentsToRemove); + } +}; \ No newline at end of file diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 5e61627..ff9b237 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -324,12 +324,12 @@ export const categories = [ icon: 'download', subtitle: 'Extract all embedded files from PDF(s) as a ZIP.', }, - // { - // id: 'edit-attachments', - // name: 'Edit Attachments', - // icon: 'file-edit', - // subtitle: 'View, remove, or replace attachments in your PDF.', - // }, + { + id: 'edit-attachments', + name: 'Edit Attachments', + icon: 'file-edit', + subtitle: 'View or remove attachments in your PDF.', + }, { href: '/src/pages/pdf-multi-tool.html', name: 'PDF Multi Tool', diff --git a/src/js/logic/edit-attachments.ts b/src/js/logic/edit-attachments.ts index 223401c..098ccd1 100644 --- a/src/js/logic/edit-attachments.ts +++ b/src/js/logic/edit-attachments.ts @@ -1,207 +1,218 @@ -// TODO@ALAM - USE CPDF HERE +import { showLoader, hideLoader, showAlert } from '../ui.js'; +import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; +import { state } from '../state.js'; -// import { showLoader, hideLoader, showAlert } from '../ui.js'; -// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; -// import { state } from '../state.js'; -// import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +const worker = new Worker('/workers/edit-attachments.worker.js'); -// let currentAttachments: Array<{ name: string; index: number; size: number }> = []; -// let attachmentsToRemove: Set = new Set(); -// let attachmentsToReplace: Map = new Map(); +let allAttachments: Array<{ index: number; name: string; page: number; data: Uint8Array }> = []; +let attachmentsToRemove: Set = new Set(); -// export async function setupEditAttachmentsTool() { -// const optionsDiv = document.getElementById('edit-attachments-options'); -// if (!optionsDiv || !state.pdfDoc) return; +export async function setupEditAttachmentsTool() { + const optionsDiv = document.getElementById('edit-attachments-options'); + if (!optionsDiv || !state.files || state.files.length === 0) return; -// optionsDiv.classList.remove('hidden'); -// await loadAttachmentsList(); -// } + optionsDiv.classList.remove('hidden'); + await loadAttachmentsList(); +} -// async function loadAttachmentsList() { -// const attachmentsList = document.getElementById('attachments-list'); -// if (!attachmentsList || !state.pdfDoc) return; +async function loadAttachmentsList() { + const attachmentsList = document.getElementById('attachments-list'); + if (!attachmentsList || !state.files || state.files.length === 0) return; -// attachmentsList.innerHTML = ''; -// currentAttachments = []; -// attachmentsToRemove.clear(); -// attachmentsToReplace.clear(); + attachmentsList.innerHTML = ''; + attachmentsToRemove.clear(); + allAttachments = []; -// try { -// // Get embedded files from PDF -// const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() -// .filter(([ref, obj]: any) => { -// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null; -// return dict && dict.get('Type')?.toString() === '/Filespec'; -// }); + try { + showLoader('Loading attachments...'); -// if (embeddedFiles.length === 0) { -// attachmentsList.innerHTML = '

No attachments found in this PDF.

'; -// return; -// } + const file = state.files[0]; + const fileBuffer = await readFileAsArrayBuffer(file); -// let index = 0; -// for (const [ref, fileSpec] of embeddedFiles) { -// try { -// const fileSpecDict = fileSpec as any; -// const fileName = fileSpecDict.get('UF')?.decodeText() || -// fileSpecDict.get('F')?.decodeText() || -// `attachment-${index + 1}`; - -// const ef = fileSpecDict.get('EF'); -// let fileSize = 0; -// if (ef) { -// const fRef = ef.get('F') || ef.get('UF'); -// if (fRef) { -// const fileStream = state.pdfDoc.context.lookup(fRef); -// if (fileStream) { -// fileSize = (fileStream as any).getContents().length; -// } -// } -// } + const message = { + command: 'get-attachments', + fileBuffer: fileBuffer, + fileName: file.name + }; -// currentAttachments.push({ name: fileName, index, size: fileSize }); + worker.postMessage(message, [fileBuffer]); + } catch (error) { + console.error('Error loading attachments:', error); + hideLoader(); + showAlert('Error', 'Failed to load attachments from PDF.'); + } +} -// const attachmentDiv = document.createElement('div'); -// attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700'; -// attachmentDiv.dataset.attachmentIndex = index.toString(); -// const infoDiv = document.createElement('div'); -// infoDiv.className = 'flex-1'; -// const nameSpan = document.createElement('span'); -// nameSpan.className = 'text-white font-medium block'; -// nameSpan.textContent = fileName; -// const sizeSpan = document.createElement('span'); -// sizeSpan.className = 'text-gray-400 text-sm'; -// sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`; -// infoDiv.append(nameSpan, sizeSpan); +worker.onmessage = (e) => { + const data = e.data; -// const actionsDiv = document.createElement('div'); -// actionsDiv.className = 'flex items-center gap-2'; + if (data.status === 'success' && data.attachments !== undefined) { + const attachments = data.attachments; + allAttachments = attachments.map(att => ({ + ...att, + data: new Uint8Array(att.data) + })); -// // Remove button -// const removeBtn = document.createElement('button'); -// removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm'; -// removeBtn.innerHTML = ''; -// removeBtn.title = 'Remove attachment'; -// removeBtn.onclick = () => { -// attachmentsToRemove.add(index); -// attachmentDiv.classList.add('opacity-50', 'line-through'); -// removeBtn.disabled = true; -// }; + displayAttachments(attachments); + hideLoader(); + } else if (data.status === 'success' && data.modifiedPDF !== undefined) { + hideLoader(); -// // Replace button -// const replaceBtn = document.createElement('button'); -// replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm'; -// replaceBtn.innerHTML = ''; -// replaceBtn.title = 'Replace attachment'; -// replaceBtn.onclick = () => { -// const input = document.createElement('input'); -// input.type = 'file'; -// input.onchange = async (e) => { -// const file = (e.target as HTMLInputElement).files?.[0]; -// if (file) { -// attachmentsToReplace.set(index, file); -// nameSpan.textContent = `${fileName} → ${file.name}`; -// nameSpan.classList.add('text-yellow-400'); -// } -// }; -// input.click(); -// }; + downloadFile( + new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }), + `edited-attachments-${data.fileName}` + ); -// actionsDiv.append(replaceBtn, removeBtn); -// attachmentDiv.append(infoDiv, actionsDiv); -// attachmentsList.appendChild(attachmentDiv); -// index++; -// } catch (e) { -// console.warn(`Failed to process attachment ${index}:`, e); -// index++; -// } -// } -// } catch (e) { -// console.error('Error loading attachments:', e); -// showAlert('Error', 'Failed to load attachments from PDF.'); -// } -// } + showAlert('Success', 'Attachments updated successfully!'); + } else if (data.status === 'error') { + hideLoader(); + showAlert('Error', data.message || 'Unknown error occurred.'); + } +}; -// export async function editAttachments() { -// if (!state.pdfDoc) { -// showAlert('Error', 'PDF is not loaded.'); -// return; -// } +worker.onerror = (error) => { + hideLoader(); + console.error('Worker error:', error); + showAlert('Error', 'Worker error occurred. Check console for details.'); +}; -// showLoader('Updating attachments...'); -// try { -// // Create a new PDF document -// const newPdfDoc = await PDFLibDocument.create(); - -// // Copy all pages -// const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices()); -// pages.forEach((page: any) => newPdfDoc.addPage(page)); +function displayAttachments(attachments) { + const attachmentsList = document.getElementById('attachments-list'); + if (!attachmentsList) return; -// // Handle attachments -// const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() -// .filter(([ref, obj]: any) => { -// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null; -// return dict && dict.get('Type')?.toString() === '/Filespec'; -// }); + const existingControls = attachmentsList.querySelector('.attachments-controls'); + attachmentsList.innerHTML = ''; + if (existingControls) { + attachmentsList.appendChild(existingControls); + } -// let attachmentIndex = 0; -// for (const [ref, fileSpec] of embeddedFiles) { -// if (attachmentsToRemove.has(attachmentIndex)) { -// attachmentIndex++; -// continue; // Skip removed attachments -// } + if (attachments.length === 0) { + const noAttachments = document.createElement('p'); + noAttachments.className = 'text-gray-400 text-center py-4'; + noAttachments.textContent = 'No attachments found in this PDF.'; + attachmentsList.appendChild(noAttachments); + return; + } -// if (attachmentsToReplace.has(attachmentIndex)) { -// // Replace attachment -// const replacementFile = attachmentsToReplace.get(attachmentIndex)!; -// const fileBytes = await readFileAsArrayBuffer(replacementFile); -// await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, { -// mimeType: replacementFile.type || 'application/octet-stream', -// description: `Attached file: ${replacementFile.name}`, -// creationDate: new Date(), -// modificationDate: new Date(replacementFile.lastModified), -// }); -// } else { -// // Keep existing attachment - copy it -// try { -// const fileSpecDict = fileSpec as any; -// const fileName = fileSpecDict.get('UF')?.decodeText() || -// fileSpecDict.get('F')?.decodeText() || -// `attachment-${attachmentIndex + 1}`; - -// const ef = fileSpecDict.get('EF'); -// if (ef) { -// const fRef = ef.get('F') || ef.get('UF'); -// if (fRef) { -// const fileStream = state.pdfDoc.context.lookup(fRef); -// if (fileStream) { -// const fileData = (fileStream as any).getContents(); -// await newPdfDoc.attach(fileData, fileName, { -// mimeType: 'application/octet-stream', -// description: `Attached file: ${fileName}`, -// }); -// } -// } -// } -// } catch (e) { -// console.warn(`Failed to copy attachment ${attachmentIndex}:`, e); -// } -// } -// attachmentIndex++; -// } + const controlsContainer = document.createElement('div'); + controlsContainer.className = 'attachments-controls mb-4 flex justify-end'; + const removeAllBtn = document.createElement('button'); + removeAllBtn.className = 'btn bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm'; + removeAllBtn.textContent = 'Remove All Attachments'; + removeAllBtn.onclick = () => { + if (allAttachments.length === 0) return; -// const pdfBytes = await newPdfDoc.save(); -// downloadFile( -// new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), -// `edited-attachments-${state.files[0].name}` -// ); -// showAlert('Success', 'Attachments updated successfully!'); -// } catch (e) { -// console.error(e); -// showAlert('Error', 'Failed to edit attachments.'); -// } finally { -// hideLoader(); -// } -// } + const allSelected = allAttachments.every(attachment => attachmentsToRemove.has(attachment.index)); + if (allSelected) { + allAttachments.forEach(attachment => { + attachmentsToRemove.delete(attachment.index); + const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`); + if (element) { + element.classList.remove('opacity-50', 'line-through'); + const removeBtn = element.querySelector('button'); + if (removeBtn) { + removeBtn.classList.remove('bg-gray-600'); + removeBtn.classList.add('bg-red-600'); + } + } + }); + removeAllBtn.textContent = 'Remove All Attachments'; + } else { + allAttachments.forEach(attachment => { + attachmentsToRemove.add(attachment.index); + const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`); + if (element) { + element.classList.add('opacity-50', 'line-through'); + const removeBtn = element.querySelector('button'); + if (removeBtn) { + removeBtn.classList.add('bg-gray-600'); + removeBtn.classList.remove('bg-red-600'); + } + } + }); + removeAllBtn.textContent = 'Deselect All'; + } + }; + + controlsContainer.appendChild(removeAllBtn); + attachmentsList.appendChild(controlsContainer); + + for (const attachment of attachments) { + const attachmentDiv = document.createElement('div'); + attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700'; + attachmentDiv.dataset.attachmentIndex = attachment.index.toString(); + + const infoDiv = document.createElement('div'); + infoDiv.className = 'flex-1'; + + const nameSpan = document.createElement('span'); + nameSpan.className = 'text-white font-medium block'; + nameSpan.textContent = attachment.name; + + const levelSpan = document.createElement('span'); + levelSpan.className = 'text-gray-400 text-sm block'; + if (attachment.page === 0) { + levelSpan.textContent = 'Document-level attachment'; + } else { + levelSpan.textContent = `Page ${attachment.page} attachment`; + } + + infoDiv.append(nameSpan, levelSpan); + + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'flex items-center gap-2'; + + const removeBtn = document.createElement('button'); + removeBtn.className = `btn ${attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`; + removeBtn.innerHTML = ''; + removeBtn.title = 'Remove attachment'; + removeBtn.onclick = () => { + if (attachmentsToRemove.has(attachment.index)) { + attachmentsToRemove.delete(attachment.index); + attachmentDiv.classList.remove('opacity-50', 'line-through'); + removeBtn.classList.remove('bg-gray-600'); + removeBtn.classList.add('bg-red-600'); + } else { + attachmentsToRemove.add(attachment.index); + attachmentDiv.classList.add('opacity-50', 'line-through'); + removeBtn.classList.add('bg-gray-600'); + removeBtn.classList.remove('bg-red-600'); + } + const allSelected = allAttachments.every(attachment => attachmentsToRemove.has(attachment.index)); + removeAllBtn.textContent = allSelected ? 'Deselect All' : 'Remove All Attachments'; + }; + + actionsDiv.append(removeBtn); + attachmentDiv.append(infoDiv, actionsDiv); + attachmentsList.appendChild(attachmentDiv); + } +} + +export async function editAttachments() { + if (!state.files || state.files.length === 0) { + showAlert('Error', 'No PDF file loaded.'); + return; + } + + showLoader('Processing attachments...'); + + try { + const file = state.files[0]; + const fileBuffer = await readFileAsArrayBuffer(file); + + const message = { + command: 'edit-attachments', + fileBuffer: fileBuffer, + fileName: file.name, + attachmentsToRemove: Array.from(attachmentsToRemove) + }; + + worker.postMessage(message, [fileBuffer]); + } catch (error) { + console.error('Error editing attachments:', error); + hideLoader(); + showAlert('Error', 'Failed to edit attachments.'); + } +} \ No newline at end of file diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index 1b50560..d4b708b 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -64,7 +64,7 @@ import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js'; import { linearizePdf } from './linearize.js'; import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js'; import { extractAttachments } from './extract-attachments.js'; -// import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js'; +import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js'; import { sanitizePdf } from './sanitize-pdf.js'; import { removeRestrictions } from './remove-restrictions.js'; @@ -141,9 +141,9 @@ export const toolLogic = { setup: setupAddAttachmentsTool, }, 'extract-attachments': extractAttachments, - // 'edit-attachments': { - // process: editAttachments, - // setup: setupEditAttachmentsTool, - // }, + 'edit-attachments': { + process: editAttachments, + setup: setupEditAttachmentsTool, + }, 'sanitize-pdf': sanitizePdf, };