Compare commits

...
4 Commits
Author SHA1 Message Date
abdullahalam123 18ecaf4228 Release v1.6.1 2025-11-11 17:51:40 +05:30
abdullahalam123 fe10335fde Merge branch 'attachment-fix' 2025-11-11 17:51:28 +05:30
abdullahalam123 842d940801 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.
2025-11-11 12:21:11 +05:30
abdullahalam123 85ce0c97b5 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.
2025-11-11 11:13:06 +05:30
9 changed files with 648 additions and 278 deletions

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"private": true, "private": true,
"version": "1.6.0", "version": "1.6.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
+42
View File
@@ -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;
+151
View File
@@ -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);
}
};
+19
View File
@@ -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;
@@ -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);
}
};
+12 -13
View File
@@ -318,19 +318,18 @@ export const categories = [
icon: 'paperclip', icon: 'paperclip',
subtitle: 'Embed one or more files into your PDF.', subtitle: 'Embed one or more files into your PDF.',
}, },
// TODO@ALAM - MAKE THIS LATER, ONCE INTEGERATED WITH CPDF {
// { id: 'extract-attachments',
// id: 'extract-attachments', name: 'Extract Attachments',
// name: 'Extract Attachments', icon: 'download',
// icon: 'download', subtitle: 'Extract all embedded files from PDF(s) as a ZIP.',
// subtitle: 'Extract all embedded files from PDF(s) as a ZIP.', },
// }, {
// { id: 'edit-attachments',
// id: 'edit-attachments', name: 'Edit Attachments',
// name: 'Edit Attachments', icon: 'file-edit',
// icon: 'file-edit', subtitle: 'View or remove attachments in your PDF.',
// subtitle: 'View, remove, or replace attachments in your PDF.', },
// },
{ {
href: '/src/pages/pdf-multi-tool.html', href: '/src/pages/pdf-multi-tool.html',
name: 'PDF Multi Tool', name: 'PDF Multi Tool',
+192 -181
View File
@@ -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'; const worker = new Worker('/workers/edit-attachments.worker.js');
// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
// import { state } from '../state.js';
// import { PDFDocument as PDFLibDocument } from 'pdf-lib';
// let currentAttachments: Array<{ name: string; index: number; size: number }> = []; let allAttachments: Array<{ index: number; name: string; page: number; data: Uint8Array }> = [];
// let attachmentsToRemove: Set<number> = new Set(); let attachmentsToRemove: Set<number> = new Set();
// let attachmentsToReplace: Map<number, File> = new Map();
// export async function setupEditAttachmentsTool() { export async function setupEditAttachmentsTool() {
// const optionsDiv = document.getElementById('edit-attachments-options'); const optionsDiv = document.getElementById('edit-attachments-options');
// if (!optionsDiv || !state.pdfDoc) return; if (!optionsDiv || !state.files || state.files.length === 0) return;
// optionsDiv.classList.remove('hidden'); optionsDiv.classList.remove('hidden');
// await loadAttachmentsList(); await loadAttachmentsList();
// } }
// async function loadAttachmentsList() { async function loadAttachmentsList() {
// const attachmentsList = document.getElementById('attachments-list'); const attachmentsList = document.getElementById('attachments-list');
// if (!attachmentsList || !state.pdfDoc) return; if (!attachmentsList || !state.files || state.files.length === 0) return;
// attachmentsList.innerHTML = ''; attachmentsList.innerHTML = '';
// currentAttachments = []; attachmentsToRemove.clear();
// attachmentsToRemove.clear(); allAttachments = [];
// attachmentsToReplace.clear();
// try { try {
// // Get embedded files from PDF showLoader('Loading 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';
// });
// if (embeddedFiles.length === 0) { const file = state.files[0];
// attachmentsList.innerHTML = '<p class="text-gray-400 text-center py-4">No attachments found in this PDF.</p>'; const fileBuffer = await readFileAsArrayBuffer(file);
// return;
// }
// let index = 0; const message = {
// for (const [ref, fileSpec] of embeddedFiles) { command: 'get-attachments',
// try { fileBuffer: fileBuffer,
// const fileSpecDict = fileSpec as any; fileName: file.name
// const fileName = fileSpecDict.get('UF')?.decodeText() || };
// fileSpecDict.get('F')?.decodeText() ||
// `attachment-${index + 1}`;
// const ef = fileSpecDict.get('EF'); worker.postMessage(message, [fileBuffer]);
// let fileSize = 0; } catch (error) {
// if (ef) { console.error('Error loading attachments:', error);
// const fRef = ef.get('F') || ef.get('UF'); hideLoader();
// if (fRef) { showAlert('Error', 'Failed to load attachments from PDF.');
// const fileStream = state.pdfDoc.context.lookup(fRef); }
// if (fileStream) { }
// fileSize = (fileStream as any).getContents().length;
// }
// }
// }
// currentAttachments.push({ name: fileName, index, size: fileSize });
// const attachmentDiv = document.createElement('div'); worker.onmessage = (e) => {
// attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700'; const data = e.data;
// attachmentDiv.dataset.attachmentIndex = index.toString();
// const infoDiv = document.createElement('div'); if (data.status === 'success' && data.attachments !== undefined) {
// infoDiv.className = 'flex-1'; const attachments = data.attachments;
// const nameSpan = document.createElement('span'); allAttachments = attachments.map(att => ({
// nameSpan.className = 'text-white font-medium block'; ...att,
// nameSpan.textContent = fileName; data: new Uint8Array(att.data)
// const sizeSpan = document.createElement('span'); }));
// sizeSpan.className = 'text-gray-400 text-sm';
// sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
// infoDiv.append(nameSpan, sizeSpan);
// const actionsDiv = document.createElement('div'); displayAttachments(attachments);
// actionsDiv.className = 'flex items-center gap-2'; hideLoader();
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
// // Remove button downloadFile(
// const removeBtn = document.createElement('button'); new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
// removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm'; `edited-attachments-${data.fileName}`
// removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>'; );
// removeBtn.title = 'Remove attachment';
// removeBtn.onclick = () => {
// attachmentsToRemove.add(index);
// attachmentDiv.classList.add('opacity-50', 'line-through');
// removeBtn.disabled = true;
// };
// // Replace button showAlert('Success', 'Attachments updated successfully!');
// const replaceBtn = document.createElement('button'); } else if (data.status === 'error') {
// replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm'; hideLoader();
// replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>'; showAlert('Error', data.message || 'Unknown error occurred.');
// 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();
// };
// actionsDiv.append(replaceBtn, removeBtn); worker.onerror = (error) => {
// attachmentDiv.append(infoDiv, actionsDiv); hideLoader();
// attachmentsList.appendChild(attachmentDiv); console.error('Worker error:', error);
// index++; showAlert('Error', 'Worker error occurred. Check console for details.');
// } 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.');
// }
// }
// export async function editAttachments() { function displayAttachments(attachments) {
// if (!state.pdfDoc) { const attachmentsList = document.getElementById('attachments-list');
// showAlert('Error', 'PDF is not loaded.'); if (!attachmentsList) return;
// return;
// }
// showLoader('Updating attachments...'); const existingControls = attachmentsList.querySelector('.attachments-controls');
// try { attachmentsList.innerHTML = '';
// // Create a new PDF document if (existingControls) {
// const newPdfDoc = await PDFLibDocument.create(); attachmentsList.appendChild(existingControls);
}
// // Copy all pages if (attachments.length === 0) {
// const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices()); const noAttachments = document.createElement('p');
// pages.forEach((page: any) => newPdfDoc.addPage(page)); noAttachments.className = 'text-gray-400 text-center py-4';
noAttachments.textContent = 'No attachments found in this PDF.';
attachmentsList.appendChild(noAttachments);
return;
}
// // Handle attachments const controlsContainer = document.createElement('div');
// const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
// .filter(([ref, obj]: any) => { const removeAllBtn = document.createElement('button');
// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null; removeAllBtn.className = 'btn bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
// return dict && dict.get('Type')?.toString() === '/Filespec'; removeAllBtn.textContent = 'Remove All Attachments';
// }); removeAllBtn.onclick = () => {
if (allAttachments.length === 0) return;
// let attachmentIndex = 0; const allSelected = allAttachments.every(attachment => attachmentsToRemove.has(attachment.index));
// for (const [ref, fileSpec] of embeddedFiles) {
// if (attachmentsToRemove.has(attachmentIndex)) {
// attachmentIndex++;
// continue; // Skip removed attachments
// }
// if (attachmentsToReplace.has(attachmentIndex)) { if (allSelected) {
// // Replace attachment allAttachments.forEach(attachment => {
// const replacementFile = attachmentsToReplace.get(attachmentIndex)!; attachmentsToRemove.delete(attachment.index);
// const fileBytes = await readFileAsArrayBuffer(replacementFile); const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
// await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, { if (element) {
// mimeType: replacementFile.type || 'application/octet-stream', element.classList.remove('opacity-50', 'line-through');
// description: `Attached file: ${replacementFile.name}`, const removeBtn = element.querySelector('button');
// creationDate: new Date(), if (removeBtn) {
// modificationDate: new Date(replacementFile.lastModified), removeBtn.classList.remove('bg-gray-600');
// }); removeBtn.classList.add('bg-red-600');
// } else { }
// // Keep existing attachment - copy it }
// try { });
// const fileSpecDict = fileSpec as any; removeAllBtn.textContent = 'Remove All Attachments';
// const fileName = fileSpecDict.get('UF')?.decodeText() || } else {
// fileSpecDict.get('F')?.decodeText() || allAttachments.forEach(attachment => {
// `attachment-${attachmentIndex + 1}`; 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';
}
};
// const ef = fileSpecDict.get('EF'); controlsContainer.appendChild(removeAllBtn);
// if (ef) { attachmentsList.appendChild(controlsContainer);
// 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 pdfBytes = await newPdfDoc.save(); for (const attachment of attachments) {
// downloadFile( const attachmentDiv = document.createElement('div');
// new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
// `edited-attachments-${state.files[0].name}` attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
// );
// showAlert('Success', 'Attachments updated successfully!');
// } catch (e) {
// console.error(e);
// showAlert('Error', 'Failed to edit attachments.');
// } finally {
// hideLoader();
// }
// }
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 = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
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.');
}
}
+118 -76
View File
@@ -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'; const worker = new Worker('/workers/extract-attachments.worker.js');
// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
// import { state } from '../state.js';
// import { PDFDocument as PDFLibDocument } from 'pdf-lib';
// import JSZip from 'jszip';
// export async function extractAttachments() { interface ExtractAttachmentSuccessResponse {
// if (state.files.length === 0) { status: 'success';
// showAlert('No Files', 'Please select at least one PDF file.'); attachments: Array<{ name: string; data: ArrayBuffer }>;
// return; }
// }
// showLoader('Extracting attachments...'); interface ExtractAttachmentErrorResponse {
// try { status: 'error';
// const zip = new JSZip(); message: string;
// let totalAttachments = 0; }
// for (const file of state.files) { type ExtractAttachmentResponse = ExtractAttachmentSuccessResponse | ExtractAttachmentErrorResponse;
// const pdfBytes = await readFileAsArrayBuffer(file);
// const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
// ignoreEncryption: true,
// });
// const embeddedFiles = pdfDoc.context.enumerateIndirectObjects() export async function extractAttachments() {
// .filter(([ref, obj]: any) => { if (state.files.length === 0) {
// // obj must be a PDFDict showStatus('No Files', 'error');
// if (obj && typeof obj.get === 'function') { return;
// const type = obj.get('Type'); }
// return type && type.toString() === '/Filespec';
// }
// return false;
// });
// if (embeddedFiles.length === 0) { document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
// console.warn(`No attachments found in ${file.name}`); document.getElementById('process-btn')?.setAttribute('disabled', 'true');
// continue;
// }
// // Extract attachments showStatus('Reading files (Main Thread)...', 'info');
// 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 try {
// const fileName = fileSpecDict.get('UF')?.decodeText() || const fileBuffers: ArrayBuffer[] = [];
// fileSpecDict.get('F')?.decodeText() || const fileNames: string[] = [];
// `attachment-${i + 1}`;
// // Get embedded file stream for (const file of state.files) {
// const ef = fileSpecDict.get('EF'); const buffer = await file.arrayBuffer();
// if (ef) { fileBuffers.push(buffer);
// const fRef = ef.get('F') || ef.get('UF'); fileNames.push(file.name);
// 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);
// }
// }
// }
// if (totalAttachments === 0) { showStatus(`Extracting attachments from ${state.files.length} file(s)...`, 'info');
// showAlert('No Attachments', 'No attachments were found in the selected PDF(s).');
// hideLoader();
// return;
// }
// const zipBlob = await zip.generateAsync({ type: 'blob' }); const message: ExtractAttachmentsMessage = {
// downloadFile(zipBlob, 'extracted-attachments.zip'); command: 'extract-attachments',
// showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`); fileBuffers,
// } catch (e) { fileNames,
// console.error(e); };
// showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
// } finally {
// hideLoader();
// }
// }
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<ExtractAttachmentResponse>) => {
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[];
}
+7 -7
View File
@@ -63,8 +63,8 @@ import {
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js'; import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
import { linearizePdf } from './linearize.js'; import { linearizePdf } from './linearize.js';
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.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 { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
import { sanitizePdf } from './sanitize-pdf.js'; import { sanitizePdf } from './sanitize-pdf.js';
import { removeRestrictions } from './remove-restrictions.js'; import { removeRestrictions } from './remove-restrictions.js';
@@ -140,10 +140,10 @@ export const toolLogic = {
process: addAttachments, process: addAttachments,
setup: setupAddAttachmentsTool, setup: setupAddAttachmentsTool,
}, },
// 'extract-attachments': extractAttachments, 'extract-attachments': extractAttachments,
// 'edit-attachments': { 'edit-attachments': {
// process: editAttachments, process: editAttachments,
// setup: setupEditAttachmentsTool, setup: setupEditAttachmentsTool,
// }, },
'sanitize-pdf': sanitizePdf, 'sanitize-pdf': sanitizePdf,
}; };