refactor: streamline HTML structure and enhance UI components
- Cleaned up the HTML structure in index.html and pdf-multi-tool.html for better readability and maintainability. - Improved the user interface of the PDF multi-tool with responsive button designs and better layout. - Added new CSS styles for button states and cursor behavior. - Updated README with corrected Docker deployment link. - Refactored JavaScript logic to utilize new helper functions for formatting star counts. - Commented out unused attachment functionalities in the logic files for future integration.
This commit is contained in:
@@ -105,7 +105,7 @@ You can run BentoPDF locally for development or personal use.
|
|||||||
|
|
||||||
### 🚀 Quick Start with Docker
|
### 🚀 Quick Start with Docker
|
||||||
|
|
||||||
[](https://zeabur.com/templates/LWO8I0?referralCode=LokiSalmonNeko)
|
[](https://zeabur.com/templates/K4AU2B)
|
||||||
|
|
||||||
You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository:
|
You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository:
|
||||||
|
|
||||||
|
|||||||
1582
index.html
1582
index.html
File diff suppressed because it is too large
Load Diff
@@ -510,3 +510,14 @@ details > summary .icon {
|
|||||||
details[open] > summary .icon {
|
details[open] > summary .icon {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
.btn,
|
||||||
|
.btn-gradient {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled,
|
||||||
|
.btn:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
@@ -3,6 +3,12 @@ export const categories = [
|
|||||||
{
|
{
|
||||||
name: 'Popular Tools',
|
name: 'Popular Tools',
|
||||||
tools: [
|
tools: [
|
||||||
|
{
|
||||||
|
href: '/src/pages/pdf-multi-tool.html',
|
||||||
|
name: 'PDF Multi Tool',
|
||||||
|
icon: 'pencil-ruler',
|
||||||
|
subtitle: 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'merge',
|
id: 'merge',
|
||||||
name: 'Merge PDF',
|
name: 'Merge PDF',
|
||||||
@@ -312,22 +318,23 @@ 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',
|
// {
|
||||||
name: 'Extract Attachments',
|
// id: 'extract-attachments',
|
||||||
icon: 'download',
|
// name: 'Extract Attachments',
|
||||||
subtitle: 'Extract all embedded files from PDF(s) as a ZIP.',
|
// icon: 'download',
|
||||||
},
|
// subtitle: 'Extract all embedded files from PDF(s) as a ZIP.',
|
||||||
{
|
// },
|
||||||
id: 'edit-attachments',
|
// {
|
||||||
name: 'Edit Attachments',
|
// id: 'edit-attachments',
|
||||||
icon: 'file-edit',
|
// name: 'Edit Attachments',
|
||||||
subtitle: 'View, remove, or replace attachments in your PDF.',
|
// icon: 'file-edit',
|
||||||
},
|
// 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',
|
||||||
icon: 'layers',
|
icon: 'pencil-ruler',
|
||||||
subtitle: 'Full-featured PDF editor with page management.',
|
subtitle: 'Full-featured PDF editor with page management.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,205 +1,207 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
// TODO@ALAM - USE CPDF HERE
|
||||||
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 }> = [];
|
// import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
let attachmentsToRemove: Set<number> = new Set();
|
// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
let attachmentsToReplace: Map<number, File> = new Map();
|
// import { state } from '../state.js';
|
||||||
|
// import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function setupEditAttachmentsTool() {
|
// let currentAttachments: Array<{ name: string; index: number; size: number }> = [];
|
||||||
const optionsDiv = document.getElementById('edit-attachments-options');
|
// let attachmentsToRemove: Set<number> = new Set();
|
||||||
if (!optionsDiv || !state.pdfDoc) return;
|
// let attachmentsToReplace: Map<number, File> = new Map();
|
||||||
|
|
||||||
optionsDiv.classList.remove('hidden');
|
// export async function setupEditAttachmentsTool() {
|
||||||
await loadAttachmentsList();
|
// const optionsDiv = document.getElementById('edit-attachments-options');
|
||||||
}
|
// if (!optionsDiv || !state.pdfDoc) return;
|
||||||
|
|
||||||
async function loadAttachmentsList() {
|
// optionsDiv.classList.remove('hidden');
|
||||||
const attachmentsList = document.getElementById('attachments-list');
|
// await loadAttachmentsList();
|
||||||
if (!attachmentsList || !state.pdfDoc) return;
|
// }
|
||||||
|
|
||||||
attachmentsList.innerHTML = '';
|
// async function loadAttachmentsList() {
|
||||||
currentAttachments = [];
|
// const attachmentsList = document.getElementById('attachments-list');
|
||||||
attachmentsToRemove.clear();
|
// if (!attachmentsList || !state.pdfDoc) return;
|
||||||
attachmentsToReplace.clear();
|
|
||||||
|
|
||||||
try {
|
// attachmentsList.innerHTML = '';
|
||||||
// Get embedded files from PDF
|
// currentAttachments = [];
|
||||||
const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
|
// attachmentsToRemove.clear();
|
||||||
.filter(([ref, obj]: any) => {
|
// attachmentsToReplace.clear();
|
||||||
const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
|
|
||||||
return dict && dict.get('Type')?.toString() === '/Filespec';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (embeddedFiles.length === 0) {
|
// try {
|
||||||
attachmentsList.innerHTML = '<p class="text-gray-400 text-center py-4">No attachments found in this PDF.</p>';
|
// // Get embedded files from PDF
|
||||||
return;
|
// 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';
|
||||||
|
// });
|
||||||
|
|
||||||
let index = 0;
|
// if (embeddedFiles.length === 0) {
|
||||||
for (const [ref, fileSpec] of embeddedFiles) {
|
// attachmentsList.innerHTML = '<p class="text-gray-400 text-center py-4">No attachments found in this PDF.</p>';
|
||||||
try {
|
// return;
|
||||||
const fileSpecDict = fileSpec as any;
|
// }
|
||||||
const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
|
||||||
fileSpecDict.get('F')?.decodeText() ||
|
// let index = 0;
|
||||||
`attachment-${index + 1}`;
|
// for (const [ref, fileSpec] of embeddedFiles) {
|
||||||
|
// try {
|
||||||
|
// const fileSpecDict = fileSpec as any;
|
||||||
|
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||||
|
// fileSpecDict.get('F')?.decodeText() ||
|
||||||
|
// `attachment-${index + 1}`;
|
||||||
|
|
||||||
const ef = fileSpecDict.get('EF');
|
// const ef = fileSpecDict.get('EF');
|
||||||
let fileSize = 0;
|
// let fileSize = 0;
|
||||||
if (ef) {
|
// if (ef) {
|
||||||
const fRef = ef.get('F') || ef.get('UF');
|
// const fRef = ef.get('F') || ef.get('UF');
|
||||||
if (fRef) {
|
// if (fRef) {
|
||||||
const fileStream = state.pdfDoc.context.lookup(fRef);
|
// const fileStream = state.pdfDoc.context.lookup(fRef);
|
||||||
if (fileStream) {
|
// if (fileStream) {
|
||||||
fileSize = (fileStream as any).getContents().length;
|
// fileSize = (fileStream as any).getContents().length;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
currentAttachments.push({ name: fileName, index, size: fileSize });
|
// currentAttachments.push({ name: fileName, index, size: fileSize });
|
||||||
|
|
||||||
const attachmentDiv = document.createElement('div');
|
// const attachmentDiv = document.createElement('div');
|
||||||
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
// attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
attachmentDiv.dataset.attachmentIndex = index.toString();
|
// attachmentDiv.dataset.attachmentIndex = index.toString();
|
||||||
|
|
||||||
const infoDiv = document.createElement('div');
|
// const infoDiv = document.createElement('div');
|
||||||
infoDiv.className = 'flex-1';
|
// infoDiv.className = 'flex-1';
|
||||||
const nameSpan = document.createElement('span');
|
// const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'text-white font-medium block';
|
// nameSpan.className = 'text-white font-medium block';
|
||||||
nameSpan.textContent = fileName;
|
// nameSpan.textContent = fileName;
|
||||||
const sizeSpan = document.createElement('span');
|
// const sizeSpan = document.createElement('span');
|
||||||
sizeSpan.className = 'text-gray-400 text-sm';
|
// sizeSpan.className = 'text-gray-400 text-sm';
|
||||||
sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
|
// sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
|
||||||
infoDiv.append(nameSpan, sizeSpan);
|
// infoDiv.append(nameSpan, sizeSpan);
|
||||||
|
|
||||||
const actionsDiv = document.createElement('div');
|
// const actionsDiv = document.createElement('div');
|
||||||
actionsDiv.className = 'flex items-center gap-2';
|
// actionsDiv.className = 'flex items-center gap-2';
|
||||||
|
|
||||||
// Remove button
|
// // Remove button
|
||||||
const removeBtn = document.createElement('button');
|
// const removeBtn = document.createElement('button');
|
||||||
removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm';
|
// removeBtn.className = 'btn 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.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||||
removeBtn.title = 'Remove attachment';
|
// removeBtn.title = 'Remove attachment';
|
||||||
removeBtn.onclick = () => {
|
// removeBtn.onclick = () => {
|
||||||
attachmentsToRemove.add(index);
|
// attachmentsToRemove.add(index);
|
||||||
attachmentDiv.classList.add('opacity-50', 'line-through');
|
// attachmentDiv.classList.add('opacity-50', 'line-through');
|
||||||
removeBtn.disabled = true;
|
// removeBtn.disabled = true;
|
||||||
};
|
// };
|
||||||
|
|
||||||
// Replace button
|
// // Replace button
|
||||||
const replaceBtn = document.createElement('button');
|
// const replaceBtn = document.createElement('button');
|
||||||
replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm';
|
// replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm';
|
||||||
replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>';
|
// replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>';
|
||||||
replaceBtn.title = 'Replace attachment';
|
// replaceBtn.title = 'Replace attachment';
|
||||||
replaceBtn.onclick = () => {
|
// replaceBtn.onclick = () => {
|
||||||
const input = document.createElement('input');
|
// const input = document.createElement('input');
|
||||||
input.type = 'file';
|
// input.type = 'file';
|
||||||
input.onchange = async (e) => {
|
// input.onchange = async (e) => {
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
// const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
if (file) {
|
// if (file) {
|
||||||
attachmentsToReplace.set(index, file);
|
// attachmentsToReplace.set(index, file);
|
||||||
nameSpan.textContent = `${fileName} → ${file.name}`;
|
// nameSpan.textContent = `${fileName} → ${file.name}`;
|
||||||
nameSpan.classList.add('text-yellow-400');
|
// nameSpan.classList.add('text-yellow-400');
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
input.click();
|
// input.click();
|
||||||
};
|
// };
|
||||||
|
|
||||||
actionsDiv.append(replaceBtn, removeBtn);
|
// actionsDiv.append(replaceBtn, removeBtn);
|
||||||
attachmentDiv.append(infoDiv, actionsDiv);
|
// attachmentDiv.append(infoDiv, actionsDiv);
|
||||||
attachmentsList.appendChild(attachmentDiv);
|
// attachmentsList.appendChild(attachmentDiv);
|
||||||
index++;
|
// index++;
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
console.warn(`Failed to process attachment ${index}:`, e);
|
// console.warn(`Failed to process attachment ${index}:`, e);
|
||||||
index++;
|
// index++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
console.error('Error loading attachments:', e);
|
// console.error('Error loading attachments:', e);
|
||||||
showAlert('Error', 'Failed to load attachments from PDF.');
|
// showAlert('Error', 'Failed to load attachments from PDF.');
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
export async function editAttachments() {
|
// export async function editAttachments() {
|
||||||
if (!state.pdfDoc) {
|
// if (!state.pdfDoc) {
|
||||||
showAlert('Error', 'PDF is not loaded.');
|
// showAlert('Error', 'PDF is not loaded.');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
showLoader('Updating attachments...');
|
// showLoader('Updating attachments...');
|
||||||
try {
|
// try {
|
||||||
// Create a new PDF document
|
// // Create a new PDF document
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
// const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
// Copy all pages
|
// // Copy all pages
|
||||||
const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices());
|
// const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices());
|
||||||
pages.forEach((page: any) => newPdfDoc.addPage(page));
|
// pages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||||
|
|
||||||
// Handle attachments
|
// // Handle attachments
|
||||||
const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
|
// const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
|
||||||
.filter(([ref, obj]: any) => {
|
// .filter(([ref, obj]: any) => {
|
||||||
const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
|
// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
|
||||||
return dict && dict.get('Type')?.toString() === '/Filespec';
|
// return dict && dict.get('Type')?.toString() === '/Filespec';
|
||||||
});
|
// });
|
||||||
|
|
||||||
let attachmentIndex = 0;
|
// let attachmentIndex = 0;
|
||||||
for (const [ref, fileSpec] of embeddedFiles) {
|
// for (const [ref, fileSpec] of embeddedFiles) {
|
||||||
if (attachmentsToRemove.has(attachmentIndex)) {
|
// if (attachmentsToRemove.has(attachmentIndex)) {
|
||||||
attachmentIndex++;
|
// attachmentIndex++;
|
||||||
continue; // Skip removed attachments
|
// continue; // Skip removed attachments
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (attachmentsToReplace.has(attachmentIndex)) {
|
// if (attachmentsToReplace.has(attachmentIndex)) {
|
||||||
// Replace attachment
|
// // Replace attachment
|
||||||
const replacementFile = attachmentsToReplace.get(attachmentIndex)!;
|
// const replacementFile = attachmentsToReplace.get(attachmentIndex)!;
|
||||||
const fileBytes = await readFileAsArrayBuffer(replacementFile);
|
// const fileBytes = await readFileAsArrayBuffer(replacementFile);
|
||||||
await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, {
|
// await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, {
|
||||||
mimeType: replacementFile.type || 'application/octet-stream',
|
// mimeType: replacementFile.type || 'application/octet-stream',
|
||||||
description: `Attached file: ${replacementFile.name}`,
|
// description: `Attached file: ${replacementFile.name}`,
|
||||||
creationDate: new Date(),
|
// creationDate: new Date(),
|
||||||
modificationDate: new Date(replacementFile.lastModified),
|
// modificationDate: new Date(replacementFile.lastModified),
|
||||||
});
|
// });
|
||||||
} else {
|
// } else {
|
||||||
// Keep existing attachment - copy it
|
// // Keep existing attachment - copy it
|
||||||
try {
|
// try {
|
||||||
const fileSpecDict = fileSpec as any;
|
// const fileSpecDict = fileSpec as any;
|
||||||
const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||||
fileSpecDict.get('F')?.decodeText() ||
|
// fileSpecDict.get('F')?.decodeText() ||
|
||||||
`attachment-${attachmentIndex + 1}`;
|
// `attachment-${attachmentIndex + 1}`;
|
||||||
|
|
||||||
const ef = fileSpecDict.get('EF');
|
// const ef = fileSpecDict.get('EF');
|
||||||
if (ef) {
|
// if (ef) {
|
||||||
const fRef = ef.get('F') || ef.get('UF');
|
// const fRef = ef.get('F') || ef.get('UF');
|
||||||
if (fRef) {
|
// if (fRef) {
|
||||||
const fileStream = state.pdfDoc.context.lookup(fRef);
|
// const fileStream = state.pdfDoc.context.lookup(fRef);
|
||||||
if (fileStream) {
|
// if (fileStream) {
|
||||||
const fileData = (fileStream as any).getContents();
|
// const fileData = (fileStream as any).getContents();
|
||||||
await newPdfDoc.attach(fileData, fileName, {
|
// await newPdfDoc.attach(fileData, fileName, {
|
||||||
mimeType: 'application/octet-stream',
|
// mimeType: 'application/octet-stream',
|
||||||
description: `Attached file: ${fileName}`,
|
// description: `Attached file: ${fileName}`,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
console.warn(`Failed to copy attachment ${attachmentIndex}:`, e);
|
// console.warn(`Failed to copy attachment ${attachmentIndex}:`, e);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
attachmentIndex++;
|
// attachmentIndex++;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const pdfBytes = await newPdfDoc.save();
|
// const pdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(
|
// downloadFile(
|
||||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
// new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
`edited-attachments-${state.files[0].name}`
|
// `edited-attachments-${state.files[0].name}`
|
||||||
);
|
// );
|
||||||
showAlert('Success', 'Attachments updated successfully!');
|
// showAlert('Success', 'Attachments updated successfully!');
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
console.error(e);
|
// console.error(e);
|
||||||
showAlert('Error', 'Failed to edit attachments.');
|
// showAlert('Error', 'Failed to edit attachments.');
|
||||||
} finally {
|
// } finally {
|
||||||
hideLoader();
|
// hideLoader();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +1,88 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
// TODO@ALAM - USE CPDF HERE
|
||||||
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() {
|
// import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
if (state.files.length === 0) {
|
// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
showAlert('No Files', 'Please select at least one PDF file.');
|
// import { state } from '../state.js';
|
||||||
return;
|
// import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
}
|
// import JSZip from 'jszip';
|
||||||
|
|
||||||
showLoader('Extracting attachments...');
|
// export async function extractAttachments() {
|
||||||
try {
|
// if (state.files.length === 0) {
|
||||||
const zip = new JSZip();
|
// showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
let totalAttachments = 0;
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
for (const file of state.files) {
|
// showLoader('Extracting attachments...');
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
// try {
|
||||||
const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
// const zip = new JSZip();
|
||||||
ignoreEncryption: true,
|
// let totalAttachments = 0;
|
||||||
});
|
|
||||||
|
|
||||||
const embeddedFiles = pdfDoc.context.enumerateIndirectObjects()
|
// for (const file of state.files) {
|
||||||
.filter(([ref, obj]: any) => {
|
// const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
// obj must be a PDFDict
|
// const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||||
if (obj && typeof obj.get === 'function') {
|
// ignoreEncryption: true,
|
||||||
const type = obj.get('Type');
|
// });
|
||||||
return type && type.toString() === '/Filespec';
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (embeddedFiles.length === 0) {
|
// const embeddedFiles = pdfDoc.context.enumerateIndirectObjects()
|
||||||
console.warn(`No attachments found in ${file.name}`);
|
// .filter(([ref, obj]: any) => {
|
||||||
continue;
|
// // obj must be a PDFDict
|
||||||
}
|
// if (obj && typeof obj.get === 'function') {
|
||||||
|
// const type = obj.get('Type');
|
||||||
|
// return type && type.toString() === '/Filespec';
|
||||||
|
// }
|
||||||
|
// return false;
|
||||||
|
// });
|
||||||
|
|
||||||
// Extract attachments
|
// if (embeddedFiles.length === 0) {
|
||||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
// console.warn(`No attachments found in ${file.name}`);
|
||||||
for (let i = 0; i < embeddedFiles.length; i++) {
|
// continue;
|
||||||
try {
|
// }
|
||||||
const [ref, fileSpec] = embeddedFiles[i];
|
|
||||||
const fileSpecDict = fileSpec as any;
|
// // Extract attachments
|
||||||
|
// const baseName = file.name.replace(/\.pdf$/i, '');
|
||||||
|
// for (let i = 0; i < embeddedFiles.length; i++) {
|
||||||
|
// try {
|
||||||
|
// const [ref, fileSpec] = embeddedFiles[i];
|
||||||
|
// const fileSpecDict = fileSpec as any;
|
||||||
|
|
||||||
// Get attachment name
|
// // Get attachment name
|
||||||
const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||||
fileSpecDict.get('F')?.decodeText() ||
|
// fileSpecDict.get('F')?.decodeText() ||
|
||||||
`attachment-${i + 1}`;
|
// `attachment-${i + 1}`;
|
||||||
|
|
||||||
// Get embedded file stream
|
// // Get embedded file stream
|
||||||
const ef = fileSpecDict.get('EF');
|
// const ef = fileSpecDict.get('EF');
|
||||||
if (ef) {
|
// if (ef) {
|
||||||
const fRef = ef.get('F') || ef.get('UF');
|
// const fRef = ef.get('F') || ef.get('UF');
|
||||||
if (fRef) {
|
// if (fRef) {
|
||||||
const fileStream = pdfDoc.context.lookup(fRef);
|
// const fileStream = pdfDoc.context.lookup(fRef);
|
||||||
if (fileStream) {
|
// if (fileStream) {
|
||||||
const fileData = (fileStream as any).getContents();
|
// const fileData = (fileStream as any).getContents();
|
||||||
zip.file(`${baseName}_${fileName}`, fileData);
|
// zip.file(`${baseName}_${fileName}`, fileData);
|
||||||
totalAttachments++;
|
// totalAttachments++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e);
|
// console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (totalAttachments === 0) {
|
// if (totalAttachments === 0) {
|
||||||
showAlert('No Attachments', 'No attachments were found in the selected PDF(s).');
|
// showAlert('No Attachments', 'No attachments were found in the selected PDF(s).');
|
||||||
hideLoader();
|
// hideLoader();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
// const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'extracted-attachments.zip');
|
// downloadFile(zipBlob, 'extracted-attachments.zip');
|
||||||
showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`);
|
// showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`);
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
console.error(e);
|
// console.error(e);
|
||||||
showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
|
// showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
|
||||||
} finally {
|
// } finally {
|
||||||
hideLoader();
|
// hideLoader();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
// @TODO:@ALAM- sometimes I think... and then I forget...
|
||||||
|
//
|
||||||
|
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
import { downloadFile } from '../utils/helpers';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -34,7 +38,7 @@ const redoStack: Snapshot[] = [];
|
|||||||
|
|
||||||
function snapshot() {
|
function snapshot() {
|
||||||
const snap: Snapshot = {
|
const snap: Snapshot = {
|
||||||
allPages: allPages.map(p => ({ ...p })),
|
allPages: allPages.map(p => ({ ...p, canvas: p.canvas })),
|
||||||
selectedPages: Array.from(selectedPages),
|
selectedPages: Array.from(selectedPages),
|
||||||
splitMarkers: Array.from(splitMarkers),
|
splitMarkers: Array.from(splitMarkers),
|
||||||
};
|
};
|
||||||
@@ -43,7 +47,10 @@ function snapshot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function restore(snap: Snapshot) {
|
function restore(snap: Snapshot) {
|
||||||
allPages = snap.allPages.map(p => ({ ...p }));
|
allPages = snap.allPages.map(p => ({
|
||||||
|
...p,
|
||||||
|
canvas: p.canvas
|
||||||
|
}));
|
||||||
selectedPages = new Set(snap.selectedPages);
|
selectedPages = new Set(snap.selectedPages);
|
||||||
splitMarkers = new Set(snap.splitMarkers);
|
splitMarkers = new Set(snap.splitMarkers);
|
||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
@@ -202,7 +209,6 @@ function initializeTool() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Modal close button
|
|
||||||
document.getElementById('modal-close-btn')?.addEventListener('click', hideModal);
|
document.getElementById('modal-close-btn')?.addEventListener('click', hideModal);
|
||||||
document.getElementById('modal')?.addEventListener('click', (e) => {
|
document.getElementById('modal')?.addEventListener('click', (e) => {
|
||||||
if (e.target === document.getElementById('modal')) {
|
if (e.target === document.getElementById('modal')) {
|
||||||
@@ -210,7 +216,6 @@ function initializeTool() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Drag and drop
|
|
||||||
const uploadArea = document.getElementById('upload-area');
|
const uploadArea = document.getElementById('upload-area');
|
||||||
if (uploadArea) {
|
if (uploadArea) {
|
||||||
uploadArea.addEventListener('dragover', (e) => {
|
uploadArea.addEventListener('dragover', (e) => {
|
||||||
@@ -230,7 +235,6 @@ function initializeTool() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show upload area initially
|
|
||||||
document.getElementById('upload-area')?.classList.remove('hidden');
|
document.getElementById('upload-area')?.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +319,6 @@ async function loadPdfs(files: File[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCacheKey(pdfIndex: number, pageIndex: number): string {
|
function getCacheKey(pdfIndex: number, pageIndex: number): string {
|
||||||
// Removed rotation from cache key - canvas is always rendered at 0 degrees
|
|
||||||
return `${pdfIndex}-${pageIndex}`;
|
return `${pdfIndex}-${pageIndex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,12 +333,10 @@ async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: n
|
|||||||
if (pageCanvasCache.has(cacheKey)) {
|
if (pageCanvasCache.has(cacheKey)) {
|
||||||
canvas = pageCanvasCache.get(cacheKey)!;
|
canvas = pageCanvasCache.get(cacheKey)!;
|
||||||
} else {
|
} else {
|
||||||
// Render page preview at 0 degrees rotation using pdfjs
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
|
const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
|
||||||
const page = await pdf.getPage(pageIndex + 1);
|
const page = await pdf.getPage(pageIndex + 1);
|
||||||
|
|
||||||
// Always render at 0 rotation - visual rotation is applied via CSS
|
|
||||||
const viewport = page.getViewport({ scale: 0.5, rotation: 0 });
|
const viewport = page.getViewport({ scale: 0.5, rotation: 0 });
|
||||||
|
|
||||||
canvas = document.createElement('canvas');
|
canvas = document.createElement('canvas');
|
||||||
@@ -384,20 +385,14 @@ function createPageCard(pageData: PageData, index: number) {
|
|||||||
const preview = document.createElement('div');
|
const preview = document.createElement('div');
|
||||||
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
|
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
|
||||||
preview.style.minHeight = '160px';
|
preview.style.minHeight = '160px';
|
||||||
preview.style.maxHeight = '256px';
|
preview.style.height = '250px';
|
||||||
|
|
||||||
const previewCanvas = pageData.canvas;
|
const previewCanvas = pageData.canvas;
|
||||||
previewCanvas.className = 'max-w-full max-h-full object-contain';
|
previewCanvas.className = 'max-w-full max-h-full object-contain';
|
||||||
|
|
||||||
// Apply visual rotation using CSS transform
|
// Apply visual rotation using CSS transform
|
||||||
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
||||||
|
previewCanvas.style.transition = 'transform 0.2s ease';
|
||||||
// Adjust container dimensions based on rotation
|
|
||||||
if (pageData.visualRotation === 90 || pageData.visualRotation === 270) {
|
|
||||||
preview.style.aspectRatio = `${previewCanvas.height} / ${previewCanvas.width}`;
|
|
||||||
} else {
|
|
||||||
preview.style.aspectRatio = `${previewCanvas.width} / ${previewCanvas.height}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
preview.appendChild(previewCanvas);
|
preview.appendChild(previewCanvas);
|
||||||
|
|
||||||
@@ -408,7 +403,11 @@ function createPageCard(pageData: PageData, index: number) {
|
|||||||
|
|
||||||
// Actions toolbar
|
// Actions toolbar
|
||||||
const actions = document.createElement('div');
|
const actions = document.createElement('div');
|
||||||
actions.className = 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity';
|
actions.className = 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity absolute bottom-2 left-0 right-0';
|
||||||
|
|
||||||
|
const actionsInner = document.createElement('div');
|
||||||
|
actionsInner.className = 'flex items-center gap-1 bg-gray-900/90 rounded px-2 py-1';
|
||||||
|
actions.appendChild(actionsInner);
|
||||||
|
|
||||||
// Select checkbox
|
// Select checkbox
|
||||||
const selectBtn = document.createElement('button');
|
const selectBtn = document.createElement('button');
|
||||||
@@ -441,6 +440,7 @@ function createPageCard(pageData: PageData, index: number) {
|
|||||||
const duplicateBtn = document.createElement('button');
|
const duplicateBtn = document.createElement('button');
|
||||||
duplicateBtn.className = 'p-1 rounded hover:bg-gray-700';
|
duplicateBtn.className = 'p-1 rounded hover:bg-gray-700';
|
||||||
duplicateBtn.innerHTML = '<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>';
|
duplicateBtn.innerHTML = '<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>';
|
||||||
|
duplicateBtn.title = 'Duplicate this page';
|
||||||
duplicateBtn.onclick = (e) => {
|
duplicateBtn.onclick = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
snapshot();
|
snapshot();
|
||||||
@@ -451,6 +451,7 @@ function createPageCard(pageData: PageData, index: number) {
|
|||||||
const deleteBtn = document.createElement('button');
|
const deleteBtn = document.createElement('button');
|
||||||
deleteBtn.className = 'p-1 rounded hover:bg-gray-700';
|
deleteBtn.className = 'p-1 rounded hover:bg-gray-700';
|
||||||
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>';
|
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4 text-red-400"></i>';
|
||||||
|
deleteBtn.title = 'Delete this page';
|
||||||
deleteBtn.onclick = (e) => {
|
deleteBtn.onclick = (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
snapshot();
|
snapshot();
|
||||||
@@ -480,7 +481,7 @@ function createPageCard(pageData: PageData, index: number) {
|
|||||||
renderSplitMarkers();
|
renderSplitMarkers();
|
||||||
};
|
};
|
||||||
|
|
||||||
actions.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
|
actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
|
||||||
card.append(preview, info, actions, selectBtn);
|
card.append(preview, info, actions, selectBtn);
|
||||||
pagesContainer.appendChild(card);
|
pagesContainer.appendChild(card);
|
||||||
|
|
||||||
@@ -506,7 +507,6 @@ function setupSortable() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optimized selection that only updates the specific card
|
|
||||||
function toggleSelectOptimized(index: number) {
|
function toggleSelectOptimized(index: number) {
|
||||||
if (selectedPages.has(index)) {
|
if (selectedPages.has(index)) {
|
||||||
selectedPages.delete(index);
|
selectedPages.delete(index);
|
||||||
@@ -546,7 +546,6 @@ function deselectAll() {
|
|||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instant rotation - just update visual rotation, no re-rendering
|
|
||||||
function rotatePage(index: number, delta: number) {
|
function rotatePage(index: number, delta: number) {
|
||||||
snapshot();
|
snapshot();
|
||||||
|
|
||||||
@@ -554,7 +553,6 @@ function rotatePage(index: number, delta: number) {
|
|||||||
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
|
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
|
||||||
pageData.rotation = (pageData.rotation + delta + 360) % 360;
|
pageData.rotation = (pageData.rotation + delta + 360) % 360;
|
||||||
|
|
||||||
// Just update the specific card's transform
|
|
||||||
const pagesContainer = document.getElementById('pages-container');
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
if (!pagesContainer) return;
|
if (!pagesContainer) return;
|
||||||
|
|
||||||
@@ -566,13 +564,7 @@ function rotatePage(index: number, delta: number) {
|
|||||||
|
|
||||||
if (canvas && preview) {
|
if (canvas && preview) {
|
||||||
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
||||||
|
canvas.style.transition = 'transform 0.2s ease';
|
||||||
// Adjust container aspect ratio
|
|
||||||
if (pageData.visualRotation === 90 || pageData.visualRotation === 270) {
|
|
||||||
(preview as HTMLElement).style.aspectRatio = `${canvas.height} / ${canvas.width}`;
|
|
||||||
} else {
|
|
||||||
(preview as HTMLElement).style.aspectRatio = `${canvas.width} / ${canvas.height}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,7 +572,6 @@ function duplicatePage(index: number) {
|
|||||||
const originalPageData = allPages[index];
|
const originalPageData = allPages[index];
|
||||||
const originalCanvas = originalPageData.canvas;
|
const originalCanvas = originalPageData.canvas;
|
||||||
|
|
||||||
// Create a new canvas and copy content
|
|
||||||
const newCanvas = document.createElement('canvas');
|
const newCanvas = document.createElement('canvas');
|
||||||
newCanvas.width = originalCanvas.width;
|
newCanvas.width = originalCanvas.width;
|
||||||
newCanvas.height = originalCanvas.height;
|
newCanvas.height = originalCanvas.height;
|
||||||
@@ -603,13 +594,18 @@ function duplicatePage(index: number) {
|
|||||||
function deletePage(index: number) {
|
function deletePage(index: number) {
|
||||||
allPages.splice(index, 1);
|
allPages.splice(index, 1);
|
||||||
selectedPages.delete(index);
|
selectedPages.delete(index);
|
||||||
// Update selected indices
|
|
||||||
const newSelected = new Set<number>();
|
const newSelected = new Set<number>();
|
||||||
selectedPages.forEach(i => {
|
selectedPages.forEach(i => {
|
||||||
if (i > index) newSelected.add(i - 1);
|
if (i > index) newSelected.add(i - 1);
|
||||||
else if (i < index) newSelected.add(i);
|
else if (i < index) newSelected.add(i);
|
||||||
});
|
});
|
||||||
selectedPages = newSelected;
|
selectedPages = newSelected;
|
||||||
|
|
||||||
|
if (allPages.length === 0) {
|
||||||
|
resetAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,7 +636,6 @@ async function handleInsertPdf(e: Event) {
|
|||||||
newPages.push(allPages.pop()!);
|
newPages.push(allPages.pop()!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert pages after the specified index
|
|
||||||
allPages.splice(insertAfterIndex + 1, 0, ...newPages);
|
allPages.splice(insertAfterIndex + 1, 0, ...newPages);
|
||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -660,10 +655,8 @@ function renderSplitMarkers() {
|
|||||||
const pagesContainer = document.getElementById('pages-container');
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
if (!pagesContainer) return;
|
if (!pagesContainer) return;
|
||||||
|
|
||||||
// Remove all existing split markers
|
|
||||||
pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove());
|
pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove());
|
||||||
|
|
||||||
// Add split markers between cards
|
|
||||||
Array.from(pagesContainer.children).forEach((cardEl, i) => {
|
Array.from(pagesContainer.children).forEach((cardEl, i) => {
|
||||||
if (splitMarkers.has(i)) {
|
if (splitMarkers.has(i)) {
|
||||||
const marker = document.createElement('div');
|
const marker = document.createElement('div');
|
||||||
@@ -675,7 +668,6 @@ function renderSplitMarkers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addBlankPage() {
|
function addBlankPage() {
|
||||||
// Create a blank page
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = 595;
|
canvas.width = 595;
|
||||||
canvas.height = 842;
|
canvas.height = 842;
|
||||||
@@ -699,7 +691,6 @@ function addBlankPage() {
|
|||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instant bulk rotation - just update visual rotation
|
|
||||||
function bulkRotate(delta: number) {
|
function bulkRotate(delta: number) {
|
||||||
if (selectedPages.size === 0) {
|
if (selectedPages.size === 0) {
|
||||||
showModal('No Selection', 'Please select pages to rotate.', 'info');
|
showModal('No Selection', 'Please select pages to rotate.', 'info');
|
||||||
@@ -712,7 +703,6 @@ function bulkRotate(delta: number) {
|
|||||||
pageData.rotation = (pageData.rotation + delta + 360) % 360;
|
pageData.rotation = (pageData.rotation + delta + 360) % 360;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update display for all rotated pages
|
|
||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,6 +714,12 @@ function bulkDelete() {
|
|||||||
const indices = Array.from(selectedPages).sort((a, b) => b - a);
|
const indices = Array.from(selectedPages).sort((a, b) => b - a);
|
||||||
indices.forEach(index => allPages.splice(index, 1));
|
indices.forEach(index => allPages.splice(index, 1));
|
||||||
selectedPages.clear();
|
selectedPages.clear();
|
||||||
|
|
||||||
|
if (allPages.length === 0) {
|
||||||
|
resetAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
updatePageDisplay();
|
updatePageDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,11 +738,18 @@ function bulkDuplicate() {
|
|||||||
|
|
||||||
function bulkSplit() {
|
function bulkSplit() {
|
||||||
if (selectedPages.size === 0) {
|
if (selectedPages.size === 0) {
|
||||||
showModal('No Selection', 'Please select pages to split.', 'info');
|
showModal('No Selection', 'Please select pages to mark for splitting.', 'info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const indices = Array.from(selectedPages);
|
const indices = Array.from(selectedPages);
|
||||||
downloadPagesAsPdf(indices, 'selected-pages.pdf');
|
indices.forEach(index => {
|
||||||
|
if (!splitMarkers.has(index)) {
|
||||||
|
splitMarkers.add(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
renderSplitMarkers();
|
||||||
|
selectedPages.clear();
|
||||||
|
updatePageDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkDownload() {
|
async function bulkDownload() {
|
||||||
@@ -759,8 +762,79 @@ async function bulkDownload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadAll() {
|
async function downloadAll() {
|
||||||
const indices = Array.from({ length: allPages.length }, (_, i) => i);
|
if (allPages.length === 0) {
|
||||||
await downloadPagesAsPdf(indices, 'all-pages.pdf');
|
showModal('No Pages', 'Please upload PDFs first.', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are split markers
|
||||||
|
if (splitMarkers.size > 0) {
|
||||||
|
// Split into multiple PDFs and download as ZIP
|
||||||
|
await downloadSplitPdfs();
|
||||||
|
} else {
|
||||||
|
// Download as single PDF
|
||||||
|
const indices = Array.from({ length: allPages.length }, (_, i) => i);
|
||||||
|
await downloadPagesAsPdf(indices, 'all-pages.pdf');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadSplitPdfs() {
|
||||||
|
try {
|
||||||
|
const zip = new JSZip();
|
||||||
|
const sortedMarkers = Array.from(splitMarkers).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
// Create segments based on split markers
|
||||||
|
const segments: number[][] = [];
|
||||||
|
let currentSegment: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < allPages.length; i++) {
|
||||||
|
currentSegment.push(i);
|
||||||
|
|
||||||
|
// If this page has a split marker after it, start a new segment
|
||||||
|
if (splitMarkers.has(i)) {
|
||||||
|
segments.push(currentSegment);
|
||||||
|
currentSegment = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last segment if it has pages
|
||||||
|
if (currentSegment.length > 0) {
|
||||||
|
segments.push(currentSegment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create PDFs for each segment
|
||||||
|
for (let segIndex = 0; segIndex < segments.length; segIndex++) {
|
||||||
|
const segment = segments[segIndex];
|
||||||
|
const newPdf = await PDFLibDocument.create();
|
||||||
|
|
||||||
|
for (const index of segment) {
|
||||||
|
const pageData = allPages[index];
|
||||||
|
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
|
||||||
|
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
|
||||||
|
const page = newPdf.addPage(copiedPage);
|
||||||
|
|
||||||
|
if (pageData.rotation !== 0) {
|
||||||
|
const currentRotation = page.getRotation().angle;
|
||||||
|
page.setRotation(degrees(currentRotation + pageData.rotation));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newPdf.addPage([595, 842]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await newPdf.save();
|
||||||
|
zip.file(`document-${segIndex + 1}.pdf`, pdfBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate and download ZIP
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'split-documents.zip');
|
||||||
|
|
||||||
|
showModal('Success', `Downloaded ${segments.length} PDF files in a ZIP archive.`, 'success');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to create split PDFs:', e);
|
||||||
|
showModal('Error', 'Failed to create split PDFs.', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadPagesAsPdf(indices: number[], filename: string) {
|
async function downloadPagesAsPdf(indices: number[], filename: string) {
|
||||||
@@ -785,12 +859,8 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
|
|||||||
|
|
||||||
const pdfBytes = await newPdf.save();
|
const pdfBytes = await newPdf.save();
|
||||||
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
downloadFile(blob, filename);
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to create PDF:', e);
|
console.error('Failed to create PDF:', e);
|
||||||
showModal('Error', 'Failed to create PDF.', 'error');
|
showModal('Error', 'Failed to create PDF.', 'error');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { setupToolInterface } from './handlers/toolSelectionHandler.js';
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import '../css/styles.css';
|
import '../css/styles.css';
|
||||||
|
import { formatStars } from './utils/helpers.js';
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
@@ -274,7 +275,7 @@ const init = () => {
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.stargazers_count !== undefined) {
|
if (data.stargazers_count !== undefined) {
|
||||||
githubStarsElement.textContent = data.stargazers_count.toLocaleString();
|
githubStarsElement.textContent = formatStars(data.stargazers_count);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export const hexToRgb = (hex: any) => {
|
|||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
return result
|
return result
|
||||||
? {
|
? {
|
||||||
r: parseInt(result[1], 16) / 255,
|
r: parseInt(result[1], 16) / 255,
|
||||||
g: parseInt(result[2], 16) / 255,
|
g: parseInt(result[2], 16) / 255,
|
||||||
b: parseInt(result[3], 16) / 255,
|
b: parseInt(result[3], 16) / 255,
|
||||||
}
|
}
|
||||||
: { r: 0, g: 0, b: 0 }; // Default to black
|
: { r: 0, g: 0, b: 0 }; // Default to black
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -187,3 +187,10 @@ export function initializeIcons(): void {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatStars(num: number) {
|
||||||
|
if (num >= 1000) {
|
||||||
|
return (num / 1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
return num.toLocaleString();
|
||||||
|
};
|
||||||
@@ -32,70 +32,88 @@
|
|||||||
<!-- Main Container -->
|
<!-- Main Container -->
|
||||||
<div class="flex flex-col h-[calc(100vh-4rem)]">
|
<div class="flex flex-col h-[calc(100vh-4rem)]">
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<div class="bg-gray-800 border-b border-gray-700 p-4 flex flex-wrap items-center gap-2 overflow-x-auto">
|
<div class="bg-gray-800 border-b border-gray-700 p-2 sm:p-4 overflow-x-auto">
|
||||||
<button id="upload-pdfs-btn"
|
<div class="flex flex-wrap items-center gap-1 sm:gap-2 min-w-max">
|
||||||
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2">
|
<button id="upload-pdfs-btn"
|
||||||
<i data-lucide="upload" class="w-4 h-4"></i> Upload PDFs
|
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-4 py-1.5 sm:py-2 rounded flex items-center gap-1 sm:gap-2 text-sm">
|
||||||
</button>
|
<i data-lucide="upload" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
<div class="border-l border-gray-600 h-6 mx-2"></div>
|
<span class="hidden sm:inline">Upload PDFs</span>
|
||||||
<button id="add-blank-page-btn"
|
<span class="sm:hidden">Upload</span>
|
||||||
class="flex items-center gap-1 btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm">
|
</button>
|
||||||
<i data-lucide="file-plus" class="w-4 h-4"></i> Add Blank
|
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||||
</button>
|
<button id="add-blank-page-btn"
|
||||||
<div class="border-l border-gray-600 h-6 mx-2"></div>
|
class="flex items-center gap-1 btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm">
|
||||||
<button id="undo-btn"
|
<i data-lucide="file-plus" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
<span class="hidden sm:inline">Add Blank</span>
|
||||||
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Undo
|
</button>
|
||||||
</button>
|
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||||
<button id="redo-btn"
|
<button id="undo-btn"
|
||||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
<i data-lucide="rotate-cw" class="w-4 h-4"></i> Redo
|
<i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
</button>
|
<span class="hidden lg:inline">Undo</span>
|
||||||
<button id="reset-btn"
|
</button>
|
||||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
<button id="redo-btn"
|
||||||
<i data-lucide="refresh-ccw" class="w-4 h-4"></i> Reset
|
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
</button>
|
<i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
<div class="border-l border-gray-600 h-6 mx-2"></div>
|
<span class="hidden lg:inline">Redo</span>
|
||||||
<span class="text-gray-400 text-sm">Selection:</span>
|
</button>
|
||||||
<button id="select-all-btn"
|
<button id="reset-btn"
|
||||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
<i data-lucide="check-square" class="w-4 h-4"></i> Select All
|
<i data-lucide="refresh-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
</button>
|
<span class="hidden lg:inline">Reset</span>
|
||||||
<button id="deselect-all-btn"
|
</button>
|
||||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||||
<i data-lucide="square" class="w-4 h-4"></i> Deselect All
|
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Selection:</span>
|
||||||
</button>
|
<button id="select-all-btn"
|
||||||
<div class="border-l border-gray-600 h-6 mx-2"></div>
|
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
<span class="text-gray-400 text-sm">Bulk Actions:</span>
|
<i data-lucide="check-square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
<button id="bulk-rotate-left-btn"
|
<span class="hidden lg:inline">Select All</span>
|
||||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
</button>
|
||||||
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Rotate Left
|
<button id="deselect-all-btn"
|
||||||
</button>
|
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
<button id="bulk-rotate-btn"
|
<i data-lucide="square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
<span class="hidden lg:inline">Deselect All</span>
|
||||||
<i data-lucide="rotate-cw" class="w-4 h-4"></i> Rotate Right
|
</button>
|
||||||
</button>
|
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||||
<button id="bulk-delete-btn"
|
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Bulk:</span>
|
||||||
class="btn bg-red-600 hover:bg-red-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
<button id="bulk-rotate-left-btn"
|
||||||
<i data-lucide="trash-2" class="w-4 h-4"></i> Delete
|
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
</button>
|
<i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
<button id="bulk-duplicate-btn"
|
<span class="hidden xl:inline">Rotate Left</span>
|
||||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
</button>
|
||||||
<i data-lucide="copy" class="w-4 h-4"></i> Duplicate
|
<button id="bulk-rotate-btn"
|
||||||
</button>
|
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
<button id="bulk-split-btn"
|
<i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
<span class="hidden xl:inline">Rotate Right</span>
|
||||||
<i data-lucide="scissors" class="w-4 h-4"></i> Split
|
</button>
|
||||||
</button>
|
<button id="bulk-delete-btn"
|
||||||
<button id="bulk-download-btn"
|
class="btn bg-red-600 hover:bg-red-700 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
class="btn bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
<i data-lucide="trash-2" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
<i data-lucide="download" class="w-4 h-4"></i> Download Selected
|
<span class="hidden xl:inline">Delete</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex-1"></div>
|
<button id="bulk-duplicate-btn"
|
||||||
<button id="export-pdf-btn"
|
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2">
|
<i data-lucide="copy" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
<i data-lucide="download" class="w-4 h-4"></i> Export PDF
|
<span class="hidden xl:inline">Duplicate</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="bulk-split-btn"
|
||||||
|
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
|
<i data-lucide="scissors" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden xl:inline">Split</span>
|
||||||
|
</button>
|
||||||
|
<button id="bulk-download-btn"
|
||||||
|
class="btn bg-green-600 hover:bg-green-700 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||||
|
<i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden xl:inline">Download Selected</span>
|
||||||
|
</button>
|
||||||
|
<div class="flex-1 min-w-[20px]"></div>
|
||||||
|
<button id="export-pdf-btn"
|
||||||
|
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-4 py-1.5 sm:py-2 rounded flex items-center gap-1 sm:gap-2 text-sm">
|
||||||
|
<i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||||
|
<span class="hidden sm:inline">Export PDF</span>
|
||||||
|
<span class="sm:hidden">Export</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content Area -->
|
<!-- Content Area -->
|
||||||
|
|||||||
Reference in New Issue
Block a user