diff --git a/README.md b/README.md index de12c6c..be70414 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ You can run BentoPDF locally for development or personal use. ### πŸš€ Quick Start with Docker -[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/LWO8I0?referralCode=LokiSalmonNeko) +[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/K4AU2B) You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository: diff --git a/index.html b/index.html index 241c465..9570621 100644 --- a/index.html +++ b/index.html @@ -1,949 +1,671 @@ - - - - BentoPDF - The Privacy First PDF Toolkit - - - - - -
-
-

- The PDF Toolkit built for - privacy. -

-

Fast, Secure and Forever Free.

-
- - - No Signups - - - - Unlimited Use - - - - Works Offline - -
- - -
- -
- -
-

- Why BentoPDF? -

-
-
-
- -

No Signup

-
-

- Start instantly, no accounts or emails. -

-
-
-
- -

No Uploads

-
-

- 100% client-side, your files never leave your device. -

-
-
-
- -

Forever Free

-
-

- All tools, no trials, no paywalls. -

-
-
-
- -

No Limits

-
-

- Use as much as you want, no hidden caps. -

-
-
-
- -

Batch Processing

-
-

Handle unlimited PDFs in one go.

-
-
-
- -

Lightning Fast

-
-

- Process PDFs instantly, without waiting or delays. -

-
-
-
- -
- -
-

- Get Started with Tools -

-

Click a tool to open the file uploader

-
- -
-
-
- - - - - - - -
-
- -
-
- - - - - - - - - -
- - -
-
-

- Your data never leaves your device - - - We keep - - - - your information safe - - by following global security standards. -

-
-
- - All the processing happens locally on your device. - -
- -
- -
-
- GDPR compliance -
-

- GDPR compliance -

-

- Protects the personal data and privacy of individuals within the - European Union. -

-
- -
-
- CCPA compliance -
-

- CCPA compliance -

-

- Gives California residents rights over how their personal - information is collected, used, and shared. -

-
- -
-
- HIPAA compliance -
-

- HIPAA compliance -

-

- Sets safeguards for handling sensitive health information in the - United States healthcare system. -

-
-
-
- - - -
- -
-

- Frequently Asked Questions -

- -
- -
-

- Yes, absolutely. All tools on BentoPDF are 100% free to use, with - no file limits, no sign-ups, and no watermarks. We believe - everyone deserves access to simple, powerful PDF tools without a - paywall. -

-
-
- -
- -
-

- Your files are as secure as possible because they **never leave - your computer**. All processing happens directly in your web - browser (client-side). We never upload your files to a server, so - you maintain complete privacy and control over your documents. -

-
-
- -
- -
-

- Yes! Since BentoPDF runs entirely in your browser, it works on any - operating system with a modern web browser, including Windows, - macOS, Linux, iOS, and Android. -

-
-
- - -
- -
-

- Yes. BentoPDF is fully GDPR compliant. Since all file processing - happens locally in your browser and we never collect or transmit - your files to any server, we have no access to your data. This - ensures you are always in control of your documents. -

-
-
- - -
- -
-

- No. We never store, track, or log your files. Everything you do on - BentoPDF happens in your browser memory and disappears once you - close the page. There are no uploads, no history logs, and no - servers involved. -

-
-
- - -
- -
-

- Most PDF tools upload your files to a server for processing. - BentoPDF never does that. We use secure, modern web technology to - process your files directly in your browser. This means faster - performance, stronger privacy, and complete peace of mind. -

-
-
- - -
- -
-

- By running entirely inside your browser, BentoPDF ensures that - your files never leave your device. This eliminates the risks of - server hacks, data breaches, or unauthorized access. Your files - remain yoursβ€”always. -

-
-
- - -
- -
-

- We care about your privacy. BentoPDF does not track personal - information. We use - Simple Analytics - solely to see anonymous visit counts. This means we can know how - many users visit our site, but - we never know who you are. Simple Analytics is - fully GDPR-compliant and respects your privacy. -

-
-
-
- -
- -
-

- What Our Users Say -

-
-
-
-
-

Sarah L.

-
- β˜…β˜…β˜…β˜…β˜… -
-
-
-

- "This is the tool I've been searching for! It's fast, free, and I - love that my confidential documents never get uploaded to some - random server. A lifesaver for my freelance work." -

-
-
-
-
-

Mark Chen

-
- β˜…β˜…β˜…β˜…β˜… -
-
-
-

- "Finally, a PDF editor that just works. No ads, no sign-ups, no - nonsense. The merge tool is surprisingly powerful. I've already - bookmarked it on all my devices." -

-
-
-
-
-

Anonymous User A-35Z

-
- β˜…β˜†β˜†β˜†β˜† -
-
-
-

- "Terrible. It won't let me upload my files to the cloud. How is my - Big Data Tech Overlord supposed to know I signed a permission slip - for my kid's field trip? Useless for my data profile." -

-
-
-
-
-

Dr. Brickson

-
- β˜…β˜…β˜…β˜…β˜… -
-
-
-

- "As a researcher, data privacy is paramount. BentoPDF's - client-side processing model is exactly what my institution - recommends. It's robust, reliable, and secure. A fantastic - resource." -

-
-
-
-
-

AdTracker Pro

-
- β˜…β˜†β˜†β˜†β˜† -
-
-
-

- "This website is broken. My ad blocker says it hasn't blocked a - single tracker. How am I supposed to know if a product is good if - it's not following me around the internet for a week? 1 star." -

-
-
-
-
-

Raj P.

-
- β˜…β˜…β˜…β˜…β˜… -
-
-
-

- "Simple, elegant, and powerful. I needed to merge 50 reports, and - it handled it instantly without crashing my browser. This is what - a web tool should be. Highly recommended." -

-
-
-
- -
-
-
-

- Like My Work? -

-

- BentoPDF is a passion project, built to provide a free, private, and - powerful PDF toolkit for everyone. If you find it useful, consider - supporting its development. Every coffee helps! -

- - - - Buy Me a Coffee - -
-
- -
- - - - - - + + + + + + \ No newline at end of file diff --git a/src/css/styles.css b/src/css/styles.css index 79d53ff..d063e37 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -510,3 +510,14 @@ details > summary .icon { details[open] > summary .icon { transform: rotate(45deg); } + +button, +.btn, +.btn-gradient { + cursor: pointer; +} + +button:disabled, +.btn:disabled { + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/js/config/tools.ts b/src/js/config/tools.ts index 6f4b63a..1931f9e 100644 --- a/src/js/config/tools.ts +++ b/src/js/config/tools.ts @@ -3,6 +3,12 @@ export const categories = [ { name: 'Popular 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', name: 'Merge PDF', @@ -312,22 +318,23 @@ export const categories = [ icon: 'paperclip', subtitle: 'Embed one or more files into your PDF.', }, - { - id: 'extract-attachments', - name: 'Extract Attachments', - icon: 'download', - subtitle: 'Extract all embedded files from PDF(s) as a ZIP.', - }, - { - id: 'edit-attachments', - name: 'Edit Attachments', - icon: 'file-edit', - subtitle: 'View, remove, or replace attachments in your PDF.', - }, + // TODO@ALAM - MAKE THIS LATER, ONCE INTEGERATED WITH CPDF + // { + // id: 'extract-attachments', + // name: 'Extract Attachments', + // icon: 'download', + // subtitle: 'Extract all embedded files from PDF(s) as a ZIP.', + // }, + // { + // id: 'edit-attachments', + // name: 'Edit Attachments', + // icon: 'file-edit', + // subtitle: 'View, remove, or replace attachments in your PDF.', + // }, { href: '/src/pages/pdf-multi-tool.html', name: 'PDF Multi Tool', - icon: 'layers', + icon: 'pencil-ruler', subtitle: 'Full-featured PDF editor with page management.', }, { diff --git a/src/js/logic/edit-attachments.ts b/src/js/logic/edit-attachments.ts index c3c3759..223401c 100644 --- a/src/js/logic/edit-attachments.ts +++ b/src/js/logic/edit-attachments.ts @@ -1,205 +1,207 @@ -import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; -import { state } from '../state.js'; -import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +// TODO@ALAM - USE CPDF HERE -let currentAttachments: Array<{ name: string; index: number; size: number }> = []; -let attachmentsToRemove: Set = new Set(); -let attachmentsToReplace: Map = new Map(); +// import { showLoader, hideLoader, showAlert } from '../ui.js'; +// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; +// import { state } from '../state.js'; +// import { PDFDocument as PDFLibDocument } from 'pdf-lib'; -export async function setupEditAttachmentsTool() { - const optionsDiv = document.getElementById('edit-attachments-options'); - if (!optionsDiv || !state.pdfDoc) return; +// let currentAttachments: Array<{ name: string; index: number; size: number }> = []; +// let attachmentsToRemove: Set = new Set(); +// let attachmentsToReplace: Map = new Map(); - optionsDiv.classList.remove('hidden'); - await loadAttachmentsList(); -} +// export async function setupEditAttachmentsTool() { +// const optionsDiv = document.getElementById('edit-attachments-options'); +// if (!optionsDiv || !state.pdfDoc) return; -async function loadAttachmentsList() { - const attachmentsList = document.getElementById('attachments-list'); - if (!attachmentsList || !state.pdfDoc) return; +// optionsDiv.classList.remove('hidden'); +// await loadAttachmentsList(); +// } - attachmentsList.innerHTML = ''; - currentAttachments = []; - attachmentsToRemove.clear(); - attachmentsToReplace.clear(); +// async function loadAttachmentsList() { +// const attachmentsList = document.getElementById('attachments-list'); +// if (!attachmentsList || !state.pdfDoc) return; - try { - // Get embedded files from PDF - const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() - .filter(([ref, obj]: any) => { - const dict = obj instanceof PDFLibDocument.context.dict ? obj : null; - return dict && dict.get('Type')?.toString() === '/Filespec'; - }); +// attachmentsList.innerHTML = ''; +// currentAttachments = []; +// attachmentsToRemove.clear(); +// attachmentsToReplace.clear(); - if (embeddedFiles.length === 0) { - attachmentsList.innerHTML = '

No attachments found in this PDF.

'; - return; - } +// try { +// // Get embedded files from PDF +// const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() +// .filter(([ref, obj]: any) => { +// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null; +// return dict && dict.get('Type')?.toString() === '/Filespec'; +// }); - let index = 0; - for (const [ref, fileSpec] of embeddedFiles) { - try { - const fileSpecDict = fileSpec as any; - const fileName = fileSpecDict.get('UF')?.decodeText() || - fileSpecDict.get('F')?.decodeText() || - `attachment-${index + 1}`; +// if (embeddedFiles.length === 0) { +// attachmentsList.innerHTML = '

No attachments found in this PDF.

'; +// return; +// } + +// let index = 0; +// for (const [ref, fileSpec] of embeddedFiles) { +// try { +// const fileSpecDict = fileSpec as any; +// const fileName = fileSpecDict.get('UF')?.decodeText() || +// fileSpecDict.get('F')?.decodeText() || +// `attachment-${index + 1}`; - const ef = fileSpecDict.get('EF'); - let fileSize = 0; - if (ef) { - const fRef = ef.get('F') || ef.get('UF'); - if (fRef) { - const fileStream = state.pdfDoc.context.lookup(fRef); - if (fileStream) { - fileSize = (fileStream as any).getContents().length; - } - } - } +// const ef = fileSpecDict.get('EF'); +// let fileSize = 0; +// if (ef) { +// const fRef = ef.get('F') || ef.get('UF'); +// if (fRef) { +// const fileStream = state.pdfDoc.context.lookup(fRef); +// if (fileStream) { +// fileSize = (fileStream as any).getContents().length; +// } +// } +// } - currentAttachments.push({ name: fileName, index, size: fileSize }); +// currentAttachments.push({ name: fileName, index, size: fileSize }); - const attachmentDiv = document.createElement('div'); - attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700'; - attachmentDiv.dataset.attachmentIndex = index.toString(); +// const attachmentDiv = document.createElement('div'); +// attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700'; +// attachmentDiv.dataset.attachmentIndex = index.toString(); - const infoDiv = document.createElement('div'); - infoDiv.className = 'flex-1'; - const nameSpan = document.createElement('span'); - nameSpan.className = 'text-white font-medium block'; - nameSpan.textContent = fileName; - const sizeSpan = document.createElement('span'); - sizeSpan.className = 'text-gray-400 text-sm'; - sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`; - infoDiv.append(nameSpan, sizeSpan); +// const infoDiv = document.createElement('div'); +// infoDiv.className = 'flex-1'; +// const nameSpan = document.createElement('span'); +// nameSpan.className = 'text-white font-medium block'; +// nameSpan.textContent = fileName; +// const sizeSpan = document.createElement('span'); +// sizeSpan.className = 'text-gray-400 text-sm'; +// sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`; +// infoDiv.append(nameSpan, sizeSpan); - const actionsDiv = document.createElement('div'); - actionsDiv.className = 'flex items-center gap-2'; +// const actionsDiv = document.createElement('div'); +// actionsDiv.className = 'flex items-center gap-2'; - // Remove button - const removeBtn = document.createElement('button'); - removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm'; - removeBtn.innerHTML = ''; - removeBtn.title = 'Remove attachment'; - removeBtn.onclick = () => { - attachmentsToRemove.add(index); - attachmentDiv.classList.add('opacity-50', 'line-through'); - removeBtn.disabled = true; - }; +// // Remove button +// const removeBtn = document.createElement('button'); +// removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm'; +// removeBtn.innerHTML = ''; +// removeBtn.title = 'Remove attachment'; +// removeBtn.onclick = () => { +// attachmentsToRemove.add(index); +// attachmentDiv.classList.add('opacity-50', 'line-through'); +// removeBtn.disabled = true; +// }; - // Replace button - const replaceBtn = document.createElement('button'); - replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm'; - replaceBtn.innerHTML = ''; - replaceBtn.title = 'Replace attachment'; - replaceBtn.onclick = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.onchange = async (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - attachmentsToReplace.set(index, file); - nameSpan.textContent = `${fileName} β†’ ${file.name}`; - nameSpan.classList.add('text-yellow-400'); - } - }; - input.click(); - }; +// // Replace button +// const replaceBtn = document.createElement('button'); +// replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm'; +// replaceBtn.innerHTML = ''; +// replaceBtn.title = 'Replace attachment'; +// replaceBtn.onclick = () => { +// const input = document.createElement('input'); +// input.type = 'file'; +// input.onchange = async (e) => { +// const file = (e.target as HTMLInputElement).files?.[0]; +// if (file) { +// attachmentsToReplace.set(index, file); +// nameSpan.textContent = `${fileName} β†’ ${file.name}`; +// nameSpan.classList.add('text-yellow-400'); +// } +// }; +// input.click(); +// }; - actionsDiv.append(replaceBtn, removeBtn); - attachmentDiv.append(infoDiv, actionsDiv); - attachmentsList.appendChild(attachmentDiv); - index++; - } catch (e) { - console.warn(`Failed to process attachment ${index}:`, e); - index++; - } - } - } catch (e) { - console.error('Error loading attachments:', e); - showAlert('Error', 'Failed to load attachments from PDF.'); - } -} +// actionsDiv.append(replaceBtn, removeBtn); +// attachmentDiv.append(infoDiv, actionsDiv); +// attachmentsList.appendChild(attachmentDiv); +// index++; +// } catch (e) { +// console.warn(`Failed to process attachment ${index}:`, e); +// index++; +// } +// } +// } catch (e) { +// console.error('Error loading attachments:', e); +// showAlert('Error', 'Failed to load attachments from PDF.'); +// } +// } -export async function editAttachments() { - if (!state.pdfDoc) { - showAlert('Error', 'PDF is not loaded.'); - return; - } +// export async function editAttachments() { +// if (!state.pdfDoc) { +// showAlert('Error', 'PDF is not loaded.'); +// return; +// } - showLoader('Updating attachments...'); - try { - // Create a new PDF document - const newPdfDoc = await PDFLibDocument.create(); +// showLoader('Updating attachments...'); +// try { +// // Create a new PDF document +// const newPdfDoc = await PDFLibDocument.create(); - // Copy all pages - const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices()); - pages.forEach((page: any) => newPdfDoc.addPage(page)); +// // Copy all pages +// const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices()); +// pages.forEach((page: any) => newPdfDoc.addPage(page)); - // Handle attachments - const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() - .filter(([ref, obj]: any) => { - const dict = obj instanceof PDFLibDocument.context.dict ? obj : null; - return dict && dict.get('Type')?.toString() === '/Filespec'; - }); +// // Handle attachments +// const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects() +// .filter(([ref, obj]: any) => { +// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null; +// return dict && dict.get('Type')?.toString() === '/Filespec'; +// }); - let attachmentIndex = 0; - for (const [ref, fileSpec] of embeddedFiles) { - if (attachmentsToRemove.has(attachmentIndex)) { - attachmentIndex++; - continue; // Skip removed attachments - } +// let attachmentIndex = 0; +// for (const [ref, fileSpec] of embeddedFiles) { +// if (attachmentsToRemove.has(attachmentIndex)) { +// attachmentIndex++; +// continue; // Skip removed attachments +// } - if (attachmentsToReplace.has(attachmentIndex)) { - // Replace attachment - const replacementFile = attachmentsToReplace.get(attachmentIndex)!; - const fileBytes = await readFileAsArrayBuffer(replacementFile); - await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, { - mimeType: replacementFile.type || 'application/octet-stream', - description: `Attached file: ${replacementFile.name}`, - creationDate: new Date(), - modificationDate: new Date(replacementFile.lastModified), - }); - } else { - // Keep existing attachment - copy it - try { - const fileSpecDict = fileSpec as any; - const fileName = fileSpecDict.get('UF')?.decodeText() || - fileSpecDict.get('F')?.decodeText() || - `attachment-${attachmentIndex + 1}`; +// if (attachmentsToReplace.has(attachmentIndex)) { +// // Replace attachment +// const replacementFile = attachmentsToReplace.get(attachmentIndex)!; +// const fileBytes = await readFileAsArrayBuffer(replacementFile); +// await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, { +// mimeType: replacementFile.type || 'application/octet-stream', +// description: `Attached file: ${replacementFile.name}`, +// creationDate: new Date(), +// modificationDate: new Date(replacementFile.lastModified), +// }); +// } else { +// // Keep existing attachment - copy it +// try { +// const fileSpecDict = fileSpec as any; +// const fileName = fileSpecDict.get('UF')?.decodeText() || +// fileSpecDict.get('F')?.decodeText() || +// `attachment-${attachmentIndex + 1}`; - const ef = fileSpecDict.get('EF'); - if (ef) { - const fRef = ef.get('F') || ef.get('UF'); - if (fRef) { - const fileStream = state.pdfDoc.context.lookup(fRef); - if (fileStream) { - const fileData = (fileStream as any).getContents(); - await newPdfDoc.attach(fileData, fileName, { - mimeType: 'application/octet-stream', - description: `Attached file: ${fileName}`, - }); - } - } - } - } catch (e) { - console.warn(`Failed to copy attachment ${attachmentIndex}:`, e); - } - } - attachmentIndex++; - } +// const ef = fileSpecDict.get('EF'); +// if (ef) { +// const fRef = ef.get('F') || ef.get('UF'); +// if (fRef) { +// const fileStream = state.pdfDoc.context.lookup(fRef); +// if (fileStream) { +// const fileData = (fileStream as any).getContents(); +// await newPdfDoc.attach(fileData, fileName, { +// mimeType: 'application/octet-stream', +// description: `Attached file: ${fileName}`, +// }); +// } +// } +// } +// } catch (e) { +// console.warn(`Failed to copy attachment ${attachmentIndex}:`, e); +// } +// } +// attachmentIndex++; +// } - const pdfBytes = await newPdfDoc.save(); - downloadFile( - new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), - `edited-attachments-${state.files[0].name}` - ); - showAlert('Success', 'Attachments updated successfully!'); - } catch (e) { - console.error(e); - showAlert('Error', 'Failed to edit attachments.'); - } finally { - hideLoader(); - } -} +// const pdfBytes = await newPdfDoc.save(); +// downloadFile( +// new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), +// `edited-attachments-${state.files[0].name}` +// ); +// showAlert('Success', 'Attachments updated successfully!'); +// } catch (e) { +// console.error(e); +// showAlert('Error', 'Failed to edit attachments.'); +// } finally { +// hideLoader(); +// } +// } diff --git a/src/js/logic/extract-attachments.ts b/src/js/logic/extract-attachments.ts index ae2df4f..df7c81f 100644 --- a/src/js/logic/extract-attachments.ts +++ b/src/js/logic/extract-attachments.ts @@ -1,86 +1,88 @@ -import { showLoader, hideLoader, showAlert } from '../ui.js'; -import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; -import { state } from '../state.js'; -import { PDFDocument as PDFLibDocument } from 'pdf-lib'; -import JSZip from 'jszip'; +// TODO@ALAM - USE CPDF HERE -export async function extractAttachments() { - if (state.files.length === 0) { - showAlert('No Files', 'Please select at least one PDF file.'); - return; - } +// import { showLoader, hideLoader, showAlert } from '../ui.js'; +// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; +// import { state } from '../state.js'; +// import { PDFDocument as PDFLibDocument } from 'pdf-lib'; +// import JSZip from 'jszip'; - showLoader('Extracting attachments...'); - try { - const zip = new JSZip(); - let totalAttachments = 0; +// export async function extractAttachments() { +// if (state.files.length === 0) { +// showAlert('No Files', 'Please select at least one PDF file.'); +// return; +// } - for (const file of state.files) { - const pdfBytes = await readFileAsArrayBuffer(file); - const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { - ignoreEncryption: true, - }); +// showLoader('Extracting attachments...'); +// try { +// const zip = new JSZip(); +// let totalAttachments = 0; - const embeddedFiles = pdfDoc.context.enumerateIndirectObjects() - .filter(([ref, obj]: any) => { - // obj must be a PDFDict - if (obj && typeof obj.get === 'function') { - const type = obj.get('Type'); - return type && type.toString() === '/Filespec'; - } - return false; - }); +// for (const file of state.files) { +// const pdfBytes = await readFileAsArrayBuffer(file); +// const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, { +// ignoreEncryption: true, +// }); - if (embeddedFiles.length === 0) { - console.warn(`No attachments found in ${file.name}`); - continue; - } +// const embeddedFiles = pdfDoc.context.enumerateIndirectObjects() +// .filter(([ref, obj]: any) => { +// // obj must be a PDFDict +// if (obj && typeof obj.get === 'function') { +// const type = obj.get('Type'); +// return type && type.toString() === '/Filespec'; +// } +// return false; +// }); - // 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; +// if (embeddedFiles.length === 0) { +// console.warn(`No attachments found in ${file.name}`); +// continue; +// } + +// // Extract attachments +// const baseName = file.name.replace(/\.pdf$/i, ''); +// for (let i = 0; i < embeddedFiles.length; i++) { +// try { +// const [ref, fileSpec] = embeddedFiles[i]; +// const fileSpecDict = fileSpec as any; - // Get attachment name - const fileName = fileSpecDict.get('UF')?.decodeText() || - fileSpecDict.get('F')?.decodeText() || - `attachment-${i + 1}`; +// // Get attachment name +// const fileName = fileSpecDict.get('UF')?.decodeText() || +// fileSpecDict.get('F')?.decodeText() || +// `attachment-${i + 1}`; - // Get embedded file stream - const ef = fileSpecDict.get('EF'); - if (ef) { - const fRef = ef.get('F') || ef.get('UF'); - if (fRef) { - const fileStream = pdfDoc.context.lookup(fRef); - if (fileStream) { - const fileData = (fileStream as any).getContents(); - zip.file(`${baseName}_${fileName}`, fileData); - totalAttachments++; - } - } - } - } catch (e) { - console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e); - } - } - } +// // Get embedded file stream +// const ef = fileSpecDict.get('EF'); +// if (ef) { +// const fRef = ef.get('F') || ef.get('UF'); +// if (fRef) { +// const fileStream = pdfDoc.context.lookup(fRef); +// if (fileStream) { +// const fileData = (fileStream as any).getContents(); +// zip.file(`${baseName}_${fileName}`, fileData); +// totalAttachments++; +// } +// } +// } +// } catch (e) { +// console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e); +// } +// } +// } - if (totalAttachments === 0) { - showAlert('No Attachments', 'No attachments were found in the selected PDF(s).'); - hideLoader(); - return; - } +// if (totalAttachments === 0) { +// showAlert('No Attachments', 'No attachments were found in the selected PDF(s).'); +// hideLoader(); +// return; +// } - const zipBlob = await zip.generateAsync({ type: 'blob' }); - downloadFile(zipBlob, 'extracted-attachments.zip'); - showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`); - } catch (e) { - console.error(e); - showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.'); - } finally { - hideLoader(); - } -} +// const zipBlob = await zip.generateAsync({ type: 'blob' }); +// downloadFile(zipBlob, 'extracted-attachments.zip'); +// showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`); +// } catch (e) { +// console.error(e); +// showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.'); +// } finally { +// hideLoader(); +// } +// } diff --git a/src/js/logic/index.ts b/src/js/logic/index.ts index d4b708b..780d450 100644 --- a/src/js/logic/index.ts +++ b/src/js/logic/index.ts @@ -63,8 +63,8 @@ import { import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js'; import { linearizePdf } from './linearize.js'; import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js'; -import { extractAttachments } from './extract-attachments.js'; -import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js'; +// import { extractAttachments } from './extract-attachments.js'; +// import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js'; import { sanitizePdf } from './sanitize-pdf.js'; import { removeRestrictions } from './remove-restrictions.js'; @@ -140,10 +140,10 @@ export const toolLogic = { process: addAttachments, setup: setupAddAttachmentsTool, }, - 'extract-attachments': extractAttachments, - 'edit-attachments': { - process: editAttachments, - setup: setupEditAttachmentsTool, - }, + // 'extract-attachments': extractAttachments, + // 'edit-attachments': { + // process: editAttachments, + // setup: setupEditAttachmentsTool, + // }, 'sanitize-pdf': sanitizePdf, }; diff --git a/src/js/logic/pdf-multi-tool.ts b/src/js/logic/pdf-multi-tool.ts index 5ddee9b..67f6d81 100644 --- a/src/js/logic/pdf-multi-tool.ts +++ b/src/js/logic/pdf-multi-tool.ts @@ -1,8 +1,12 @@ +// @TODO:@ALAM- sometimes I think... and then I forget... +// + import { createIcons, icons } from 'lucide'; import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib'; import * as pdfjsLib from 'pdfjs-dist'; import JSZip from 'jszip'; import Sortable from 'sortablejs'; +import { downloadFile } from '../utils/helpers'; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', @@ -34,7 +38,7 @@ const redoStack: Snapshot[] = []; function snapshot() { const snap: Snapshot = { - allPages: allPages.map(p => ({ ...p })), + allPages: allPages.map(p => ({ ...p, canvas: p.canvas })), selectedPages: Array.from(selectedPages), splitMarkers: Array.from(splitMarkers), }; @@ -43,7 +47,10 @@ function 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); splitMarkers = new Set(snap.splitMarkers); updatePageDisplay(); @@ -202,7 +209,6 @@ function initializeTool() { } }); - // Modal close button document.getElementById('modal-close-btn')?.addEventListener('click', hideModal); document.getElementById('modal')?.addEventListener('click', (e) => { if (e.target === document.getElementById('modal')) { @@ -210,7 +216,6 @@ function initializeTool() { } }); - // Drag and drop const uploadArea = document.getElementById('upload-area'); if (uploadArea) { uploadArea.addEventListener('dragover', (e) => { @@ -230,7 +235,6 @@ function initializeTool() { }); } - // Show upload area initially document.getElementById('upload-area')?.classList.remove('hidden'); } @@ -315,7 +319,6 @@ async function loadPdfs(files: File[]) { } function getCacheKey(pdfIndex: number, pageIndex: number): string { - // Removed rotation from cache key - canvas is always rendered at 0 degrees return `${pdfIndex}-${pageIndex}`; } @@ -330,12 +333,10 @@ async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: n if (pageCanvasCache.has(cacheKey)) { canvas = pageCanvasCache.get(cacheKey)!; } else { - // Render page preview at 0 degrees rotation using pdfjs const pdfBytes = await pdfDoc.save(); const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise; 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 }); canvas = document.createElement('canvas'); @@ -384,20 +385,14 @@ function createPageCard(pageData: PageData, index: number) { const preview = document.createElement('div'); preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative'; preview.style.minHeight = '160px'; - preview.style.maxHeight = '256px'; + preview.style.height = '250px'; const previewCanvas = pageData.canvas; previewCanvas.className = 'max-w-full max-h-full object-contain'; // Apply visual rotation using CSS transform previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`; - - // 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}`; - } + previewCanvas.style.transition = 'transform 0.2s ease'; preview.appendChild(previewCanvas); @@ -408,7 +403,11 @@ function createPageCard(pageData: PageData, index: number) { // Actions toolbar 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 const selectBtn = document.createElement('button'); @@ -441,6 +440,7 @@ function createPageCard(pageData: PageData, index: number) { const duplicateBtn = document.createElement('button'); duplicateBtn.className = 'p-1 rounded hover:bg-gray-700'; duplicateBtn.innerHTML = ''; + duplicateBtn.title = 'Duplicate this page'; duplicateBtn.onclick = (e) => { e.stopPropagation(); snapshot(); @@ -451,6 +451,7 @@ function createPageCard(pageData: PageData, index: number) { const deleteBtn = document.createElement('button'); deleteBtn.className = 'p-1 rounded hover:bg-gray-700'; deleteBtn.innerHTML = ''; + deleteBtn.title = 'Delete this page'; deleteBtn.onclick = (e) => { e.stopPropagation(); snapshot(); @@ -480,7 +481,7 @@ function createPageCard(pageData: PageData, index: number) { renderSplitMarkers(); }; - actions.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn); + actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn); card.append(preview, info, actions, selectBtn); pagesContainer.appendChild(card); @@ -506,7 +507,6 @@ function setupSortable() { }); } -// Optimized selection that only updates the specific card function toggleSelectOptimized(index: number) { if (selectedPages.has(index)) { selectedPages.delete(index); @@ -546,7 +546,6 @@ function deselectAll() { updatePageDisplay(); } -// Instant rotation - just update visual rotation, no re-rendering function rotatePage(index: number, delta: number) { snapshot(); @@ -554,7 +553,6 @@ function rotatePage(index: number, delta: number) { pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360; pageData.rotation = (pageData.rotation + delta + 360) % 360; - // Just update the specific card's transform const pagesContainer = document.getElementById('pages-container'); if (!pagesContainer) return; @@ -566,13 +564,7 @@ function rotatePage(index: number, delta: number) { if (canvas && preview) { canvas.style.transform = `rotate(${pageData.visualRotation}deg)`; - - // 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}`; - } + canvas.style.transition = 'transform 0.2s ease'; } } @@ -580,7 +572,6 @@ function duplicatePage(index: number) { const originalPageData = allPages[index]; const originalCanvas = originalPageData.canvas; - // Create a new canvas and copy content const newCanvas = document.createElement('canvas'); newCanvas.width = originalCanvas.width; newCanvas.height = originalCanvas.height; @@ -603,13 +594,18 @@ function duplicatePage(index: number) { function deletePage(index: number) { allPages.splice(index, 1); selectedPages.delete(index); - // Update selected indices const newSelected = new Set(); selectedPages.forEach(i => { if (i > index) newSelected.add(i - 1); else if (i < index) newSelected.add(i); }); selectedPages = newSelected; + + if (allPages.length === 0) { + resetAll(); + return; + } + updatePageDisplay(); } @@ -640,7 +636,6 @@ async function handleInsertPdf(e: Event) { newPages.push(allPages.pop()!); } - // Insert pages after the specified index allPages.splice(insertAfterIndex + 1, 0, ...newPages); updatePageDisplay(); } catch (e) { @@ -660,10 +655,8 @@ function renderSplitMarkers() { const pagesContainer = document.getElementById('pages-container'); if (!pagesContainer) return; - // Remove all existing split markers pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove()); - // Add split markers between cards Array.from(pagesContainer.children).forEach((cardEl, i) => { if (splitMarkers.has(i)) { const marker = document.createElement('div'); @@ -675,7 +668,6 @@ function renderSplitMarkers() { } function addBlankPage() { - // Create a blank page const canvas = document.createElement('canvas'); canvas.width = 595; canvas.height = 842; @@ -699,7 +691,6 @@ function addBlankPage() { updatePageDisplay(); } -// Instant bulk rotation - just update visual rotation function bulkRotate(delta: number) { if (selectedPages.size === 0) { showModal('No Selection', 'Please select pages to rotate.', 'info'); @@ -712,7 +703,6 @@ function bulkRotate(delta: number) { pageData.rotation = (pageData.rotation + delta + 360) % 360; }); - // Update display for all rotated pages updatePageDisplay(); } @@ -724,6 +714,12 @@ function bulkDelete() { const indices = Array.from(selectedPages).sort((a, b) => b - a); indices.forEach(index => allPages.splice(index, 1)); selectedPages.clear(); + + if (allPages.length === 0) { + resetAll(); + return; + } + updatePageDisplay(); } @@ -742,11 +738,18 @@ function bulkDuplicate() { function bulkSplit() { 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; } 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() { @@ -759,8 +762,79 @@ async function bulkDownload() { } async function downloadAll() { - const indices = Array.from({ length: allPages.length }, (_, i) => i); - await downloadPagesAsPdf(indices, 'all-pages.pdf'); + if (allPages.length === 0) { + 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) { @@ -785,12 +859,8 @@ async function downloadPagesAsPdf(indices: number[], filename: string) { const pdfBytes = await newPdf.save(); const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = filename; - a.click(); - URL.revokeObjectURL(url); + + downloadFile(blob, filename); } catch (e) { console.error('Failed to create PDF:', e); showModal('Error', 'Failed to create PDF.', 'error'); diff --git a/src/js/main.ts b/src/js/main.ts index 7037864..68513e8 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -4,6 +4,7 @@ import { setupToolInterface } from './handlers/toolSelectionHandler.js'; import { createIcons, icons } from 'lucide'; import * as pdfjsLib from 'pdfjs-dist'; import '../css/styles.css'; +import { formatStars } from './utils/helpers.js'; const init = () => { pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( @@ -274,7 +275,7 @@ const init = () => { .then((response) => response.json()) .then((data) => { if (data.stargazers_count !== undefined) { - githubStarsElement.textContent = data.stargazers_count.toLocaleString(); + githubStarsElement.textContent = formatStars(data.stargazers_count); } }) .catch(() => { diff --git a/src/js/utils/helpers.ts b/src/js/utils/helpers.ts index bf92f66..e92d319 100644 --- a/src/js/utils/helpers.ts +++ b/src/js/utils/helpers.ts @@ -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); return result ? { - r: parseInt(result[1], 16) / 255, - g: parseInt(result[2], 16) / 255, - b: parseInt(result[3], 16) / 255, - } + r: parseInt(result[1], 16) / 255, + g: parseInt(result[2], 16) / 255, + b: parseInt(result[3], 16) / 255, + } : { 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(); +}; \ No newline at end of file diff --git a/src/pages/pdf-multi-tool.html b/src/pages/pdf-multi-tool.html index eb9f2bd..1275f0f 100644 --- a/src/pages/pdf-multi-tool.html +++ b/src/pages/pdf-multi-tool.html @@ -32,70 +32,88 @@
-
- -
- -
- - - -
- Selection: - - -
- Bulk Actions: - - - - - - -
- +
+
+ +
+ +
+ + + +
+ + + +
+ + + + + + + +
+ +