Compare commits

...
10 Commits
Author SHA1 Message Date
abdullahalam123 f70265136f Release v1.6.2 2025-11-13 11:26:44 +05:30
abdullahalam123 cb53370a26 Update version to 1.6.2 and enhance navigation links across HTML pages
- Updated version number in package-lock.json and relevant HTML files.
- Changed navigation links to point to the root path for consistency.
- Improved code formatting and structure in various JavaScript and HTML files for better readability.
2025-11-13 11:26:40 +05:30
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 503c5972eb Release v1.6.0 2025-11-11 17:34:11 +05:30
abdullahalam123 6bf5fad275 chore: update version number to 1.6.0 in footer 2025-11-11 17:34:03 +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
abdullahalam123 4125302d01 Release v1.5.1 2025-11-10 23:35:45 +05:30
abdullahalam123 9704e8ab96 resolve-multi-tool-path 2025-11-10 23:35:40 +05:30
23 changed files with 1416 additions and 1273 deletions

No files matched your search

+1 -1
View File
@@ -594,7 +594,7 @@
© 2025 BentoPDF. All rights reserved. © 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version">1.5.0</span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"version": "1.2.0", "version": "1.6.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bento-pdf", "name": "bento-pdf",
"version": "1.2.0", "version": "1.6.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fontsource/cedarville-cursive": "^5.2.7", "@fontsource/cedarville-cursive": "^5.2.7",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"private": true, "private": true,
"version": "1.5.0", "version": "1.6.2",
"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',
+14 -5
View File
@@ -50,14 +50,14 @@ function showInputModal(title, fields = [], defaultValues = {}) {
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label> <label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
<select id="modal-${field.name}" class="w-full px-3 py-2 border border-gray-300 rounded-lg"> <select id="modal-${field.name}" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
${field.options ${field.options
.map( .map(
(opt) => ` (opt) => `
<option value="${opt.value}" ${defaultValues[field.name] === opt.value ? 'selected' : ''}> <option value="${opt.value}" ${defaultValues[field.name] === opt.value ? 'selected' : ''}>
${opt.label} ${opt.label}
</option> </option>
` `
) )
.join('')} .join('')}
</select> </select>
${field.name === 'color' ? '<input type="color" id="modal-color-picker" class="hidden w-full h-10 mt-2 rounded cursor-pointer border border-gray-300" value="#000000" />' : ''} ${field.name === 'color' ? '<input type="color" id="modal-color-picker" class="hidden w-full h-10 mt-2 rounded cursor-pointer border border-gray-300" value="#000000" />' : ''}
</div> </div>
@@ -654,6 +654,9 @@ const fileDisplayArea = document.getElementById(
const backToToolsBtn = document.getElementById( const backToToolsBtn = document.getElementById(
'back-to-tools' 'back-to-tools'
) as HTMLButtonElement; ) as HTMLButtonElement;
const closeBtn = document.getElementById(
'back-btn'
) as HTMLButtonElement;
const canvas = document.getElementById('pdf-canvas'); const canvas = document.getElementById('pdf-canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const pageIndicator = document.getElementById('page-indicator'); const pageIndicator = document.getElementById('page-indicator');
@@ -2078,7 +2081,13 @@ async function extractExistingBookmarks(doc) {
// Back to tools button // Back to tools button
if (backToToolsBtn) { if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => { backToToolsBtn.addEventListener('click', () => {
window.location.href = '../../index.html#tools-header'; window.location.href = '/';
});
}
if (closeBtn) {
closeBtn.addEventListener('click', () => {
window.location.href = '/';
}); });
} }
+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,
}; };
+41 -42
View File
@@ -16,13 +16,12 @@ function showStatus(
type: 'success' | 'error' | 'info' = 'info' type: 'success' | 'error' | 'info' = 'info'
) { ) {
statusMessage.textContent = message statusMessage.textContent = message
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${ statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
type === 'success'
? 'bg-green-900 text-green-200' ? 'bg-green-900 text-green-200'
: type === 'error' : type === 'error'
? 'bg-red-900 text-red-200' ? 'bg-red-900 text-red-200'
: 'bg-blue-900 text-blue-200' : 'bg-blue-900 text-blue-200'
}` }`
statusMessage.classList.remove('hidden') statusMessage.classList.remove('hidden')
} }
@@ -87,9 +86,9 @@ async function convertJSONsToPDF() {
showStatus('Converting JSONs to PDFs...', 'info') showStatus('Converting JSONs to PDFs...', 'info')
worker.postMessage({ worker.postMessage({
command: 'convert', command: 'convert',
fileBuffers: fileBuffers, fileBuffers: fileBuffers,
fileNames: selectedFiles.map(f => f.name) fileNames: selectedFiles.map(f => f.name)
}, fileBuffers); }, fileBuffers);
} catch (error) { } catch (error) {
@@ -100,55 +99,55 @@ async function convertJSONsToPDF() {
} }
worker.onmessage = async (e: MessageEvent) => { worker.onmessage = async (e: MessageEvent) => {
convertBtn.disabled = false; convertBtn.disabled = false;
if (e.data.status === 'success') { if (e.data.status === 'success') {
const pdfFiles = e.data.pdfFiles as Array<{ name: string, data: ArrayBuffer }>; const pdfFiles = e.data.pdfFiles as Array<{ name: string, data: ArrayBuffer }>;
try { try {
showStatus('Creating ZIP file...', 'info') showStatus('Creating ZIP file...', 'info')
const zip = new JSZip() const zip = new JSZip()
pdfFiles.forEach(({ name, data }) => { pdfFiles.forEach(({ name, data }) => {
const pdfName = name.replace(/\.json$/i, '.pdf') const pdfName = name.replace(/\.json$/i, '.pdf')
const uint8Array = new Uint8Array(data) const uint8Array = new Uint8Array(data)
zip.file(pdfName, uint8Array) zip.file(pdfName, uint8Array)
}) })
const zipBlob = await zip.generateAsync({ type: 'blob' }) const zipBlob = await zip.generateAsync({ type: 'blob' })
const url = URL.createObjectURL(zipBlob) const url = URL.createObjectURL(zipBlob)
const a = document.createElement('a') const a = document.createElement('a')
a.href = url a.href = url
a.download = 'jsons-to-pdf.zip' a.download = 'jsons-to-pdf.zip'
downloadFile(zipBlob, 'jsons-to-pdf.zip') downloadFile(zipBlob, 'jsons-to-pdf.zip')
showStatus('✅ JSONs converted to PDF successfully! ZIP download started.', 'success') showStatus('✅ JSONs converted to PDF successfully! ZIP download started.', 'success')
selectedFiles = [] selectedFiles = []
jsonFilesInput.value = '' jsonFilesInput.value = ''
fileListDiv.innerHTML = '' fileListDiv.innerHTML = ''
fileListDiv.classList.add('hidden') fileListDiv.classList.add('hidden')
convertBtn.disabled = true convertBtn.disabled = true
setTimeout(() => { setTimeout(() => {
hideStatus() hideStatus()
}, 3000) }, 3000)
} catch (error) { } catch (error) {
console.error('Error creating ZIP:', error) console.error('Error creating ZIP:', error)
showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error') showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
}
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
showStatus(`❌ Worker Error: ${errorMessage}`, 'error');
} }
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
showStatus(`❌ Worker Error: ${errorMessage}`, 'error');
}
}; };
if (backToToolsBtn) { if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => { backToToolsBtn.addEventListener('click', () => {
window.location.href = '../../index.html#tools-header' window.location.href = '/'
}) })
} }
+20 -3
View File
@@ -29,6 +29,7 @@ let currentPdfDocs: PDFLibDocument[] = [];
let splitMarkers: Set<number> = new Set(); let splitMarkers: Set<number> = new Set();
let isRendering = false; let isRendering = false;
let renderCancelled = false; let renderCancelled = false;
let sortableInstance: Sortable | null = null;
const pageCanvasCache = new Map<string, HTMLCanvasElement>(); const pageCanvasCache = new Map<string, HTMLCanvasElement>();
@@ -114,7 +115,7 @@ function initializeTool() {
createIcons({ icons }); createIcons({ icons });
document.getElementById('close-tool-btn')?.addEventListener('click', () => { document.getElementById('close-tool-btn')?.addEventListener('click', () => {
window.location.href = '../../index.html'; window.location.href = '/';
}); });
document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => { document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => {
@@ -247,6 +248,13 @@ function resetAll() {
pageCanvasCache.clear(); pageCanvasCache.clear();
renderCancelled = false; renderCancelled = false;
isRendering = false; isRendering = false;
// Destroy sortable instance
if (sortableInstance) {
sortableInstance.destroy();
sortableInstance = null;
}
updatePageDisplay(); updatePageDisplay();
document.getElementById('upload-area')?.classList.remove('hidden'); document.getElementById('upload-area')?.classList.remove('hidden');
} }
@@ -492,9 +500,18 @@ function setupSortable() {
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; if (!pagesContainer) return;
Sortable.create(pagesContainer, { // Destroy existing instance before creating new one
if (sortableInstance) {
sortableInstance.destroy();
}
sortableInstance = Sortable.create(pagesContainer, {
animation: 150, animation: 150,
handle: '.cursor-move', forceFallback: true,
touchStartThreshold: 3,
fallbackTolerance: 3,
delay: 200,
delayOnTouchOnly: true,
onEnd: (evt) => { onEnd: (evt) => {
const oldIndex = evt.oldIndex!; const oldIndex = evt.oldIndex!;
const newIndex = evt.newIndex!; const newIndex = evt.newIndex!;
+37 -38
View File
@@ -16,13 +16,12 @@ function showStatus(
type: 'success' | 'error' | 'info' = 'info' type: 'success' | 'error' | 'info' = 'info'
) { ) {
statusMessage.textContent = message statusMessage.textContent = message
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${ statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
type === 'success'
? 'bg-green-900 text-green-200' ? 'bg-green-900 text-green-200'
: type === 'error' : type === 'error'
? 'bg-red-900 text-red-200' ? 'bg-red-900 text-red-200'
: 'bg-blue-900 text-blue-200' : 'bg-blue-900 text-blue-200'
}` }`
statusMessage.classList.remove('hidden') statusMessage.classList.remove('hidden')
} }
@@ -87,9 +86,9 @@ async function convertPDFsToJSON() {
showStatus('Converting PDFs to JSON..', 'info') showStatus('Converting PDFs to JSON..', 'info')
worker.postMessage({ worker.postMessage({
command: 'convert', command: 'convert',
fileBuffers: fileBuffers, fileBuffers: fileBuffers,
fileNames: selectedFiles.map(f => f.name) fileNames: selectedFiles.map(f => f.name)
}, fileBuffers); }, fileBuffers);
} catch (error) { } catch (error) {
@@ -100,51 +99,51 @@ async function convertPDFsToJSON() {
} }
worker.onmessage = async (e: MessageEvent) => { worker.onmessage = async (e: MessageEvent) => {
convertBtn.disabled = false; convertBtn.disabled = false;
if (e.data.status === 'success') { if (e.data.status === 'success') {
const jsonFiles = e.data.jsonFiles as Array<{ name: string, data: ArrayBuffer }>; const jsonFiles = e.data.jsonFiles as Array<{ name: string, data: ArrayBuffer }>;
try { try {
showStatus('Creating ZIP file...', 'info') showStatus('Creating ZIP file...', 'info')
const zip = new JSZip() const zip = new JSZip()
jsonFiles.forEach(({ name, data }) => { jsonFiles.forEach(({ name, data }) => {
const jsonName = name.replace(/\.pdf$/i, '.json') const jsonName = name.replace(/\.pdf$/i, '.json')
const uint8Array = new Uint8Array(data) const uint8Array = new Uint8Array(data)
zip.file(jsonName, uint8Array) zip.file(jsonName, uint8Array)
}) })
const zipBlob = await zip.generateAsync({ type: 'blob' }) const zipBlob = await zip.generateAsync({ type: 'blob' })
downloadFile(zipBlob, 'pdfs-to-json.zip') downloadFile(zipBlob, 'pdfs-to-json.zip')
showStatus('✅ PDFs converted to JSON successfully! ZIP download started.', 'success') showStatus('✅ PDFs converted to JSON successfully! ZIP download started.', 'success')
selectedFiles = [] selectedFiles = []
pdfFilesInput.value = '' pdfFilesInput.value = ''
fileListDiv.innerHTML = '' fileListDiv.innerHTML = ''
fileListDiv.classList.add('hidden') fileListDiv.classList.add('hidden')
convertBtn.disabled = true convertBtn.disabled = true
setTimeout(() => { setTimeout(() => {
hideStatus() hideStatus()
}, 3000) }, 3000)
} catch (error) { } catch (error) {
console.error('Error creating ZIP:', error) console.error('Error creating ZIP:', error)
showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error') showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
}
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
showStatus(`❌ Worker Error: ${errorMessage}`, 'error');
} }
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
showStatus(`❌ Worker Error: ${errorMessage}`, 'error');
}
}; };
if (backToToolsBtn) { if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => { backToToolsBtn.addEventListener('click', () => {
window.location.href = '../../index.html#tools-header' window.location.href = '/'
}) })
} }
+3 -4
View File
@@ -53,13 +53,12 @@ function showStatus(
type: 'success' | 'error' | 'info' = 'info' type: 'success' | 'error' | 'info' = 'info'
) { ) {
statusMessage.textContent = message; statusMessage.textContent = message;
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${ statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
type === 'success'
? 'bg-green-900 text-green-200' ? 'bg-green-900 text-green-200'
: type === 'error' : type === 'error'
? 'bg-red-900 text-red-200' ? 'bg-red-900 text-red-200'
: 'bg-blue-900 text-blue-200' : 'bg-blue-900 text-blue-200'
}`; }`;
statusMessage.classList.remove('hidden'); statusMessage.classList.remove('hidden');
} }
@@ -198,7 +197,7 @@ worker.onerror = (error) => {
if (backToToolsBtn) { if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => { backToToolsBtn.addEventListener('click', () => {
window.location.href = '../../index.html#tools-header'; window.location.href = '/';
}); });
} }
+1 -1
View File
@@ -94,7 +94,7 @@ const init = () => {
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version-simple">1.5.0</span> Version <span id="app-version-simple">1.6.2</span>
</p> </p>
</div> </div>
`; `;
+437 -688
View File
File diff suppressed because it is too large. Load diff
+99 -100
View File
@@ -1,113 +1,112 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JSON to PDF Converter - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
<link href="../../src/css/styles.css" rel="stylesheet" />
</head>
<body class="antialiased bg-gray-900">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<span class="text-white font-bold text-xl ml-2">
<a href="../../index.html">BentoPDF</a>
</span>
</div>
<!-- Desktop Navigation --> <head>
<div class="hidden md:flex items-center space-x-8 text-white"> <meta charset="UTF-8" />
<a href="../../index.html" class="nav-link">Home</a> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<a href="../../about.html" class="nav-link">About</a> <title>JSON to PDF Converter - BentoPDF</title>
<a href="../../contact.html" class="nav-link">Contact</a> <link rel="icon" type="image/png" href="../../images/favicon.svg" />
<a href="../../index.html#tools-header" class="nav-link">All Tools</a> <link href="../../src/css/styles.css" rel="stylesheet" />
</div> </head>
<!-- Mobile Hamburger Button --> <body class="antialiased bg-gray-900">
<div class="md:hidden flex items-center"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<button id="mobile-menu-button" type="button" <div class="container mx-auto px-4">
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors" <div class="flex justify-between items-center h-16">
aria-controls="mobile-menu" aria-expanded="false"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<span class="sr-only">Open main menu</span> <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <span class="text-white font-bold text-xl ml-2">
stroke="currentColor" aria-hidden="true"> <a href="/">BentoPDF</a>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> </span>
</svg>
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="../../index.html" class="mobile-nav-link">Home</a>
<a href="../../about.html" class="mobile-nav-link">About</a>
<a href="../../contact.html" class="mobile-nav-link">Contact</a>
<a href="../../index.html#tools-header" class="mobile-nav-link">All Tools</a>
</div>
</div>
</nav>
<div class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
<div class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
<button id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer"> Back to Tools </span>
</button>
<h1 class="text-2xl font-bold text-white mb-2">JSON to PDF Converter</h1>
<p class="text-gray-400 mb-6">
Upload multiple JSON files to convert them all to PDF format. Files will be downloaded as a ZIP archive.
</p>
<div class="bg-yellow-900/30 border border-yellow-700 rounded-lg p-3 mb-6">
<p class="text-yellow-200 text-sm">
<strong>Note:</strong> Only JSON files created by the PDF-to-JSON converter tool are supported. Standard JSON files from other tools will not work.
</p>
</div> </div>
<div class="upload-section mb-6"> <!-- Desktop Navigation -->
<div class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300"> <div class="hidden md:flex items-center space-x-8 text-white">
<div class="flex flex-col items-center justify-center pt-5 pb-6"> <a href="/" class="nav-link">Home</a>
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i> <a href="/about.html" class="nav-link">About</a>
<p class="mb-2 text-sm text-gray-300"> <a href="/contact.html" class="nav-link">Contact</a>
<span class="font-semibold">Click to select files</span> or drag and drop <a href="/" class="nav-link">All Tools</a>
</p> </div>
<p class="text-xs text-gray-500">Multiple JSON files</p>
<p class="text-xs text-gray-500">Your files never leave your device.</p>
</div>
<input
type="file"
id="jsonFiles"
accept="application/json"
multiple
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
<!-- File Display Area --> <!-- Mobile Hamburger Button -->
<div id="fileList" class="mt-4 hidden"></div> <div class="md:hidden flex items-center">
<button id="mobile-menu-button" type="button"
<button id="convertBtn" disabled class="btn-gradient w-full mt-6"> class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
Convert to PDF aria-controls="mobile-menu" aria-expanded="false">
<span class="sr-only">Open main menu</span>
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</div> </div>
<!-- Status Message -->
<div id="status-message" class="mt-4 hidden p-3 rounded-lg text-sm"></div>
</div> </div>
</div> </div>
<script type="module" src="../js/logic/json-to-pdf.ts"></script> <!-- Mobile Menu Dropdown -->
<script type="module" src="../js/utils/lucide-init.ts"></script> <div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
<script type="module" src="../js/mobileMenu.ts"></script> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
</body> <a href="/" class="mobile-nav-link">Home</a>
</html> <a href="/about.html" class="mobile-nav-link">About</a>
<a href="/contact.html" class="mobile-nav-link">Contact</a>
<a href="/" class="mobile-nav-link">All Tools</a>
</div>
</div>
</nav>
<div class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
<div class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
<button id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer"> Back to Tools </span>
</button>
<h1 class="text-2xl font-bold text-white mb-2">JSON to PDF Converter</h1>
<p class="text-gray-400 mb-6">
Upload multiple JSON files to convert them all to PDF format. Files will be downloaded as a ZIP archive.
</p>
<div class="bg-yellow-900/30 border border-yellow-700 rounded-lg p-3 mb-6">
<p class="text-yellow-200 text-sm">
<strong>Note:</strong> Only JSON files created by the PDF-to-JSON converter tool are supported. Standard JSON
files from other tools will not work.
</p>
</div>
<div class="upload-section mb-6">
<div
class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
<p class="mb-2 text-sm text-gray-300">
<span class="font-semibold">Click to select files</span> or drag and drop
</p>
<p class="text-xs text-gray-500">Multiple JSON files</p>
<p class="text-xs text-gray-500">Your files never leave your device.</p>
</div>
<input type="file" id="jsonFiles" accept="application/json" multiple
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
</div>
<!-- File Display Area -->
<div id="fileList" class="mt-4 hidden"></div>
<button id="convertBtn" disabled class="btn-gradient w-full mt-6">
Convert to PDF
</button>
</div>
<!-- Status Message -->
<div id="status-message" class="mt-4 hidden p-3 rounded-lg text-sm"></div>
</div>
</div>
<script type="module" src="../js/logic/json-to-pdf.ts"></script>
<script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script>
</body>
</html>
+11 -8
View File
@@ -30,7 +30,7 @@
<div class="flex-shrink-0 flex items-center"> <div class="flex-shrink-0 flex items-center">
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="../../index.html">BentoPDF</a> <a href="/">BentoPDF</a>
</span> </span>
<span class="text-gray-400 ml-3 text-sm sm:text-base">PDF Multi Tool</span> <span class="text-gray-400 ml-3 text-sm sm:text-base">PDF Multi Tool</span>
</div> </div>
@@ -60,6 +60,7 @@
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div> <div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<!-- Add Blank --> <!-- Add Blank -->
<button id="add-blank-page-btn" <button id="add-blank-page-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base"> class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
@@ -68,19 +69,19 @@
</button> </button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div> <div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Edit:</span> <span class="text-gray-400 text-xs sm:text-sm md:inline">Edit:</span>
<!-- Undo / Redo / Reset --> <!-- Undo / Redo / Reset -->
<button id="undo-btn" <button id="undo-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base"> class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i> <i data-lucide="undo-2" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Undo</span> <span class="hidden lg:inline">Undo</span>
</button> </button>
<button id="redo-btn" <button id="redo-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base"> class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i> <i data-lucide="redo-2" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Redo</span> <span class="hidden lg:inline">Redo</span>
</button> </button>
@@ -93,7 +94,7 @@
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div> <div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<!-- Selection --> <!-- Selection -->
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Selection:</span> <span class="text-gray-400 text-xs sm:text-sm md:inline">Selection:</span>
<button id="select-all-btn" <button id="select-all-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base"> class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
@@ -110,7 +111,7 @@
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div> <div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<!-- Rotate --> <!-- Rotate -->
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Rotate:</span> <span class="text-gray-400 text-xs sm:text-sm md:inline">Rotate:</span>
<button id="bulk-rotate-left-btn" <button id="bulk-rotate-left-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base"> class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i> <i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
@@ -124,6 +125,8 @@
</button> </button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div> <div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<span class="text-gray-400 text-xs sm:text-sm md:inline">Transform:</span>
<!-- Duplicate / Split --> <!-- Duplicate / Split -->
<button id="bulk-duplicate-btn" <button id="bulk-duplicate-btn"
@@ -139,7 +142,7 @@
</button> </button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div> <div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Clear:</span> <span class="text-gray-400 text-xs sm:text-sm md:inline">Clear:</span>
<!-- Delete --> <!-- Delete -->
@@ -152,7 +155,7 @@
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div> <div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<!-- Download --> <!-- Download -->
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Download:</span> <span class="text-gray-400 text-xs sm:text-sm md:inline">Download:</span>
<button id="bulk-download-btn" <button id="bulk-download-btn"
class="flex items-center gap-1 sm:gap-2 bg-green-600 hover:bg-green-700 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base"> class="flex items-center gap-1 sm:gap-2 bg-green-600 hover:bg-green-700 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i> <i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
+92 -94
View File
@@ -1,108 +1,106 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PDF to JSON Converter - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
<link href="../../src/css/styles.css" rel="stylesheet" />
</head>
<body class="antialiased bg-gray-900">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<span class="text-white font-bold text-xl ml-2">
<a href="../../index.html">BentoPDF</a>
</span>
</div>
<!-- Desktop Navigation --> <head>
<div class="hidden md:flex items-center space-x-8 text-white"> <meta charset="UTF-8" />
<a href="../../index.html" class="nav-link">Home</a> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<a href="../../about.html" class="nav-link">About</a> <title>PDF to JSON Converter - BentoPDF</title>
<a href="../../contact.html" class="nav-link">Contact</a> <link rel="icon" type="image/png" href="../../images/favicon.svg" />
<a href="../../index.html#tools-header" class="nav-link">All Tools</a> <link href="../../src/css/styles.css" rel="stylesheet" />
</div> </head>
<!-- Mobile Hamburger Button --> <body class="antialiased bg-gray-900">
<div class="md:hidden flex items-center"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<button id="mobile-menu-button" type="button" <div class="container mx-auto px-4">
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors" <div class="flex justify-between items-center h-16">
aria-controls="mobile-menu" aria-expanded="false"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<span class="sr-only">Open main menu</span> <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <span class="text-white font-bold text-xl ml-2">
stroke="currentColor" aria-hidden="true"> <a href="/">BentoPDF</a>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> </span>
</svg>
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div> </div>
</div>
<!-- Mobile Menu Dropdown --> <!-- Desktop Navigation -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div class="hidden md:flex items-center space-x-8 text-white">
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <a href="/" class="nav-link">Home</a>
<a href="../../index.html" class="mobile-nav-link">Home</a> <a href="/about.html" class="nav-link">About</a>
<a href="../../about.html" class="mobile-nav-link">About</a> <a href="/contact.html" class="nav-link">Contact</a>
<a href="../../contact.html" class="mobile-nav-link">Contact</a> <a href="/" class="nav-link">All Tools</a>
<a href="../../index.html#tools-header" class="mobile-nav-link">All Tools</a>
</div> </div>
</div>
</nav>
<div class="min-h-screen flex items-center justify-center p-4 bg-gray-900"> <!-- Mobile Hamburger Button -->
<div class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700"> <div class="md:hidden flex items-center">
<button id="back-to-tools" <button id="mobile-menu-button" type="button"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"> class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
<i data-lucide="arrow-left" class="cursor-pointer"></i> aria-controls="mobile-menu" aria-expanded="false">
<span class="cursor-pointer"> Back to Tools </span> <span class="sr-only">Open main menu</span>
</button> <svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
<h1 class="text-2xl font-bold text-white mb-2">PDF to JSON Converter</h1> stroke="currentColor" aria-hidden="true">
<p class="text-gray-400 mb-6"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
Upload multiple PDF files to convert them all to JSON format. Files will be downloaded as a ZIP archive. </svg>
</p> <svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<div class="upload-section mb-6"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<div class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300"> </svg>
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
<p class="mb-2 text-sm text-gray-300">
<span class="font-semibold">Click to select files</span> or drag and drop
</p>
<p class="text-xs text-gray-500">Multiple PDF files</p>
<p class="text-xs text-gray-500">Your files never leave your device.</p>
</div>
<input
type="file"
id="pdfFiles"
accept="application/pdf"
multiple
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
/>
</div>
<!-- File Display Area -->
<div id="fileList" class="mt-4 hidden"></div>
<button id="convertBtn" disabled class="btn-gradient w-full mt-6">
Convert to JSON
</button> </button>
</div> </div>
<!-- Status Message -->
<div id="status-message" class="mt-4 hidden p-3 rounded-lg text-sm"></div>
</div> </div>
</div> </div>
<script type="module" src="../js/logic/pdf-to-json.ts"></script> <!-- Mobile Menu Dropdown -->
<script type="module" src="../js/utils/lucide-init.ts"></script> <div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
<script type="module" src="../js/mobileMenu.ts"></script> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
</body> <a href="/" class="mobile-nav-link">Home</a>
</html> <a href="/about.html" class="mobile-nav-link">About</a>
<a href="/contact.html" class="mobile-nav-link">Contact</a>
<a href="/" class="mobile-nav-link">All Tools</a>
</div>
</div>
</nav>
<div class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
<div class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
<button id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer"> Back to Tools </span>
</button>
<h1 class="text-2xl font-bold text-white mb-2">PDF to JSON Converter</h1>
<p class="text-gray-400 mb-6">
Upload multiple PDF files to convert them all to JSON format. Files will be downloaded as a ZIP archive.
</p>
<div class="upload-section mb-6">
<div
class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
<p class="mb-2 text-sm text-gray-300">
<span class="font-semibold">Click to select files</span> or drag and drop
</p>
<p class="text-xs text-gray-500">Multiple PDF files</p>
<p class="text-xs text-gray-500">Your files never leave your device.</p>
</div>
<input type="file" id="pdfFiles" accept="application/pdf" multiple
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
</div>
<!-- File Display Area -->
<div id="fileList" class="mt-4 hidden"></div>
<button id="convertBtn" disabled class="btn-gradient w-full mt-6">
Convert to JSON
</button>
</div>
<!-- Status Message -->
<div id="status-message" class="mt-4 hidden p-3 rounded-lg text-sm"></div>
</div>
</div>
<script type="module" src="../js/logic/pdf-to-json.ts"></script>
<script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script>
</body>
</html>
+9 -9
View File
@@ -16,16 +16,16 @@
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="../../index.html">BentoPDF</a> <a href="/">BentoPDF</a>
</span> </span>
</div> </div>
<!-- Desktop Navigation --> <!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8 text-white"> <div class="hidden md:flex items-center space-x-8 text-white">
<a href="../../index.html" class="nav-link">Home</a> <a href="/" class="nav-link">Home</a>
<a href="../../about.html" class="nav-link">About</a> <a href="/about.html" class="nav-link">About</a>
<a href="../../contact.html" class="nav-link">Contact</a> <a href="/contact.html" class="nav-link">Contact</a>
<a href="../../index.html#tools-header" class="nav-link">All Tools</a> <a href="/" class="nav-link">All Tools</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
@@ -52,10 +52,10 @@
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="../../index.html" class="mobile-nav-link">Home</a> <a href="/" class="mobile-nav-link">Home</a>
<a href="../../about.html" class="mobile-nav-link">About</a> <a href="/about.html" class="mobile-nav-link">About</a>
<a href="../../contact.html" class="mobile-nav-link">Contact</a> <a href="/contact.html" class="mobile-nav-link">Contact</a>
<a href="../../index.html#tools-header" class="mobile-nav-link">All Tools</a> <a href="/" class="mobile-nav-link">All Tools</a>
</div> </div>
</div> </div>
</nav> </nav>
+1
View File
@@ -50,6 +50,7 @@ export default defineConfig(({ mode }) => ({
), ),
'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'), 'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'),
'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'), 'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'),
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
}, },
}, },
}, },