feat(pdf-tools): add attachments feature to embed files in PDFs
Implement new functionality to allow embedding attachments into PDF documents. The feature includes: - UI for selecting PDF and files to attach - Logic to embed files while preserving metadata - Display of attached files with size information - Download of modified PDF with embedded files
This commit is contained in:
125
src/js/logic/add-attachments.ts
Normal file
125
src/js/logic/add-attachments.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
|
||||
import { state } from '../state';
|
||||
let attachments: File[] = [];
|
||||
|
||||
export async function addAttachments() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'Main PDF is not loaded.');
|
||||
return;
|
||||
}
|
||||
if (attachments.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one file to attach.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Embedding files into PDF...');
|
||||
try {
|
||||
const pdfDoc = state.pdfDoc;
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
const file = attachments[i];
|
||||
showLoader(`Attaching ${file.name} (${i + 1}/${attachments.length})...`);
|
||||
|
||||
const fileBytes = await readFileAsArrayBuffer(file);
|
||||
|
||||
await pdfDoc.attach(fileBytes as ArrayBuffer, file.name, {
|
||||
mimeType: file.type || 'application/octet-stream',
|
||||
description: `Attached file: ${file.name}`,
|
||||
creationDate: new Date(),
|
||||
modificationDate: new Date(file.lastModified),
|
||||
});
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([pdfBytes], { type: 'application/pdf' }),
|
||||
`attached-${state.files[0].name}`
|
||||
);
|
||||
|
||||
showAlert(
|
||||
'Success',
|
||||
`${attachments.length} file(s) attached successfully.`
|
||||
);
|
||||
} catch (error: any) {
|
||||
console.error('Error attaching files:', error);
|
||||
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||
} finally {
|
||||
hideLoader();
|
||||
clearAttachments();
|
||||
}
|
||||
}
|
||||
|
||||
function clearAttachments() {
|
||||
attachments = [];
|
||||
const fileListDiv = document.getElementById('attachment-file-list');
|
||||
const attachmentInput = document.getElementById(
|
||||
'attachment-files-input'
|
||||
) as HTMLInputElement;
|
||||
const processBtn = document.getElementById(
|
||||
'process-btn'
|
||||
) as HTMLButtonElement;
|
||||
|
||||
if (fileListDiv) fileListDiv.innerHTML = '';
|
||||
if (attachmentInput) attachmentInput.value = '';
|
||||
if (processBtn) {
|
||||
processBtn.disabled = true;
|
||||
processBtn.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
let isSetup = false; // Prevent duplicate setup
|
||||
|
||||
export function setupAddAttachmentsTool() {
|
||||
if (isSetup) return; // Already set up
|
||||
isSetup = true;
|
||||
|
||||
const optionsDiv = document.getElementById('attachment-options');
|
||||
const attachmentInput = document.getElementById(
|
||||
'attachment-files-input'
|
||||
) as HTMLInputElement;
|
||||
const fileListDiv = document.getElementById('attachment-file-list');
|
||||
const processBtn = document.getElementById(
|
||||
'process-btn'
|
||||
) as HTMLButtonElement;
|
||||
|
||||
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
|
||||
console.error('Attachment tool UI elements not found.');
|
||||
return;
|
||||
}
|
||||
|
||||
optionsDiv.classList.remove('hidden');
|
||||
|
||||
attachmentInput.addEventListener('change', (e) => {
|
||||
const files = (e.target as HTMLInputElement).files;
|
||||
if (files && files.length > 0) {
|
||||
attachments = Array.from(files);
|
||||
|
||||
fileListDiv.innerHTML = '';
|
||||
attachments.forEach((file) => {
|
||||
const div = document.createElement('div');
|
||||
div.className =
|
||||
'flex justify-between items-center p-2 bg-gray-800 rounded-md text-white';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate text-sm';
|
||||
nameSpan.textContent = file.name;
|
||||
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'text-xs text-gray-400';
|
||||
sizeSpan.textContent = `${Math.round(file.size / 1024)} KB`;
|
||||
|
||||
div.appendChild(nameSpan);
|
||||
div.appendChild(sizeSpan);
|
||||
fileListDiv.appendChild(div);
|
||||
});
|
||||
|
||||
processBtn.disabled = false;
|
||||
processBtn.classList.remove('hidden');
|
||||
} else {
|
||||
clearAttachments();
|
||||
}
|
||||
});
|
||||
|
||||
processBtn.onclick = addAttachments;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export async function addBlankPage() {
|
||||
const pageNumberInput = document.getElementById('page-number').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageCountInput = document.getElementById('page-count').value;
|
||||
|
||||
|
||||
if (pageNumberInput.trim() === '') {
|
||||
showAlert('Invalid Input', 'Please enter a page number.');
|
||||
return;
|
||||
@@ -31,7 +31,10 @@ export async function addBlankPage() {
|
||||
}
|
||||
|
||||
if (isNaN(pageCount) || pageCount < 1) {
|
||||
showAlert('Invalid Input', 'Please enter a valid number of pages (1 or more).');
|
||||
showAlert(
|
||||
'Invalid Input',
|
||||
'Please enter a valid number of pages (1 or more).'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
} from './remove-blank-pages.js';
|
||||
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
||||
import { linearizePdf } from './linearize.js';
|
||||
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
|
||||
|
||||
export const toolLogic = {
|
||||
merge: { process: merge, setup: setupMergeTool },
|
||||
@@ -130,4 +131,8 @@ export const toolLogic = {
|
||||
setup: setupAlternateMergeTool,
|
||||
},
|
||||
linearize: linearizePdf,
|
||||
'add-attachments': {
|
||||
process: addAttachments,
|
||||
setup: setupAddAttachmentsTool,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -30,7 +30,9 @@ async function initializeQpdf() {
|
||||
|
||||
export async function linearizePdf() {
|
||||
// Check if there are files and at least one PDF
|
||||
const pdfFiles = state.files.filter((file: File) => file.type === 'application/pdf');
|
||||
const pdfFiles = state.files.filter(
|
||||
(file: File) => file.type === 'application/pdf'
|
||||
);
|
||||
if (!pdfFiles || pdfFiles.length === 0) {
|
||||
showAlert('No PDF Files', 'Please upload at least one PDF file.');
|
||||
return;
|
||||
@@ -43,12 +45,12 @@ export async function linearizePdf() {
|
||||
let errorCount = 0;
|
||||
|
||||
try {
|
||||
qpdf = await initializeQpdf();
|
||||
qpdf = await initializeQpdf();
|
||||
|
||||
for (let i = 0; i < pdfFiles.length; i++) {
|
||||
const file = pdfFiles[i];
|
||||
const inputPath = `/input_${i}.pdf`;
|
||||
const outputPath = `/output_${i}.pdf`;
|
||||
const inputPath = `/input_${i}.pdf`;
|
||||
const outputPath = `/output_${i}.pdf`;
|
||||
|
||||
showLoader(`Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`);
|
||||
|
||||
@@ -58,23 +60,20 @@ export async function linearizePdf() {
|
||||
|
||||
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||
|
||||
const args = [
|
||||
inputPath,
|
||||
'--linearize',
|
||||
outputPath,
|
||||
];
|
||||
const args = [inputPath, '--linearize', outputPath];
|
||||
|
||||
qpdf.callMain(args);
|
||||
|
||||
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
console.error(`Linearization resulted in an empty file for ${file.name}.`);
|
||||
throw new Error(`Processing failed for ${file.name}.`);
|
||||
}
|
||||
if (!outputFile || outputFile.length === 0) {
|
||||
console.error(
|
||||
`Linearization resulted in an empty file for ${file.name}.`
|
||||
);
|
||||
throw new Error(`Processing failed for ${file.name}.`);
|
||||
}
|
||||
|
||||
zip.file(`linearized-${file.name}`, outputFile, { binary: true });
|
||||
successCount++;
|
||||
|
||||
} catch (fileError: any) {
|
||||
errorCount++;
|
||||
console.error(`Failed to linearize ${file.name}:`, fileError);
|
||||
@@ -91,13 +90,16 @@ export async function linearizePdf() {
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn(`Failed to cleanup WASM FS for ${file.name}:`, cleanupError);
|
||||
console.warn(
|
||||
`Failed to cleanup WASM FS for ${file.name}:`,
|
||||
cleanupError
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount === 0) {
|
||||
throw new Error('No PDF files could be linearized.');
|
||||
throw new Error('No PDF files could be linearized.');
|
||||
}
|
||||
|
||||
showLoader('Generating ZIP file...');
|
||||
@@ -109,7 +111,6 @@ export async function linearizePdf() {
|
||||
alertMessage += ` ${errorCount} file(s) failed.`;
|
||||
}
|
||||
showAlert('Processing Complete', alertMessage);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Linearization process error:', error);
|
||||
showAlert(
|
||||
@@ -119,4 +120,4 @@ export async function linearizePdf() {
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user