Compare commits

...
15 Commits
Author SHA1 Message Date
abdullahalam123 f70265136f Release v1.6.2 2025-11-13 11:26:44 +05:30
abdullahalam123 cb53370a26 Update version to 1.6.2 and enhance navigation links across HTML pages
- Updated version number in package-lock.json and relevant HTML files.
- Changed navigation links to point to the root path for consistency.
- Improved code formatting and structure in various JavaScript and HTML files for better readability.
2025-11-13 11:26:40 +05:30
abdullahalam123 18ecaf4228 Release v1.6.1 2025-11-11 17:51:40 +05:30
abdullahalam123 fe10335fde Merge branch 'attachment-fix' 2025-11-11 17:51:28 +05:30
abdullahalam123 503c5972eb Release v1.6.0 2025-11-11 17:34:11 +05:30
abdullahalam123 6bf5fad275 chore: update version number to 1.6.0 in footer 2025-11-11 17:34:03 +05:30
abdullahalam123 842d940801 feat: implement PDF attachment editing functionality with web worker support
- Added a new worker script to handle editing of embedded attachments in PDF files.
- Created TypeScript definitions for message structures and response types related to attachment editing.
- Updated the main logic to utilize the worker for improved performance and responsiveness during attachment management.
- Integrated the editing feature into the UI, allowing users to view, remove, or replace attachments in their PDFs.
- Enhanced error handling and user feedback during the editing process.
2025-11-11 12:21:11 +05:30
abdullahalam123 85ce0c97b5 feat: implement PDF attachment extraction functionality with web worker support
- Added a new worker script to handle extraction of embedded attachments from PDF files.
- Created TypeScript definitions for the message structure and response types.
- Updated the main extraction logic to utilize the worker for improved performance and responsiveness.
- Integrated the extraction feature into the UI, allowing users to extract attachments as a ZIP file.
- Enhanced error handling and user feedback during the extraction process.
2025-11-11 11:13:06 +05:30
abdullahalam123 4125302d01 Release v1.5.1 2025-11-10 23:35:45 +05:30
abdullahalam123 9704e8ab96 resolve-multi-tool-path 2025-11-10 23:35:40 +05:30
abdullahalam123 875f552881 Release v1.5.0 2025-11-10 23:05:18 +05:30
abdullahalam123 eacee28853 style: enhance PDF multi-tool UI with responsive design and scrollbar adjustments
- Added custom CSS to hide scrollbars for better aesthetics in the toolbar.
- Updated button styles for improved responsiveness and consistency across different screen sizes.
- Enhanced layout and spacing for toolbar elements and upload area for a cleaner user experience.
- Adjusted text sizes and padding for better readability and usability.
2025-11-10 23:04:41 +05:30
abdullahalam123 7e14c83ab8 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.
2025-11-10 21:54:41 +05:30
abdullahalam123 0634600073 feat: enhance PDF tools with new features and UI improvements
- Added 'Extract Attachments' and 'Edit Attachments' functionalities to manage embedded files in PDFs.
- Introduced new splitting options: by bookmarks and by a specified number of pages (N times).
- Updated the user interface to include new options and improved layout for better usability.
- Enhanced the GitHub link display with dynamic star count retrieval.
- Bumped version to 1.4.0 and updated footer to reflect the new version.
- Refactored existing code for better maintainability and added new TypeScript definitions for the new features.
2025-11-10 18:41:48 +05:30
abdullahalam123 4b5b29bf9a Release v1.4.0 2025-11-08 15:24:07 +05:30
38 changed files with 4491 additions and 1042 deletions

No files matched your search

+1 -1
View File
@@ -105,7 +105,7 @@ You can run BentoPDF locally for development or personal use.
### 🚀 Quick Start with Docker ### 🚀 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: You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository:
+144 -395
View File
@@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -12,15 +13,8 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
class="flex-shrink-0 flex items-center cursor-pointer" <img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
id="home-logo"
>
<img
src="images/favicon.svg"
alt="Bento PDF Logo"
class="h-8 w-8"
/>
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
@@ -36,47 +30,19 @@
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center"> <div class="md:hidden flex items-center">
<button <button id="mobile-menu-button" type="button"
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu" aria-controls="mobile-menu" aria-expanded="false">
aria-expanded="false"
>
<span class="sr-only">Open main menu</span> <span class="sr-only">Open main menu</span>
<!-- Hamburger Icon --> <!-- Hamburger Icon -->
<svg <svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
id="menu-icon" stroke="currentColor" aria-hidden="true">
class="block h-6 w-6" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg> </svg>
<!-- Close Icon --> <!-- Close Icon -->
<svg <svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
id="close-icon" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
class="hidden h-6 w-6" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -84,17 +50,12 @@
</div> </div>
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div <div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a> <a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="index.html#tools-header" class="mobile-nav-link" <a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
>All Tools</a
>
</div> </div>
</div> </div>
</nav> </nav>
@@ -103,40 +64,48 @@
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4"> <h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
The <span class="marker-slanted"> PDF Toolkit </span> built for The <span class="marker-slanted"> PDF Toolkit </span> built for
privacy<span privacy<span
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500" class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
>.</span
>
</h1> </h1>
<p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p> <p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p>
<div <div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8">
class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8"
>
<span <span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
>
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-4 h-4"></i>
No Signups No Signups
</span> </span>
<span <span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
>
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-4 h-4"></i>
Unlimited Use Unlimited Use
</span> </span>
<span <span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
>
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-4 h-4"></i>
Works Offline Works Offline
</span> </span>
</div> </div>
<div class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-4">
<a <a href="#tools-header"
href="#tools-header" class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">Start
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1" Using - Forever Free</a>
>Start Using - Forever Free</a <a href="https://github.com/alam00000/bentopdf/stargazers" target="_blank" rel="noopener noreferrer" class="
> inline-flex items-center gap-1.5 text-sm font-medium
bg-white text-gray-800 border border-gray-300
dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600
pl-2.5 pr-3 py-1
rounded-full transition-colors duration-200
shadow-sm hover:shadow-md hover:bg-gray-50 dark:hover:bg-gray-700
">
<svg class="w-4 h-4 flex-shrink-0 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 24 24"
aria-hidden="true">
<path fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
<span id="github-stars">-</span>
</a>
</div> </div>
</section> </section>
@@ -149,10 +118,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="user-plus"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">No Signup</h3> <h3 class="text-xl font-bold text-white">No Signup</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -161,10 +127,7 @@
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="shield" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="shield"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">No Uploads</h3> <h3 class="text-xl font-bold text-white">No Uploads</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -173,10 +136,7 @@
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="badge-dollar-sign"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">Forever Free</h3> <h3 class="text-xl font-bold text-white">Forever Free</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -185,10 +145,7 @@
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="infinity" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="infinity"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">No Limits</h3> <h3 class="text-xl font-bold text-white">No Limits</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -197,20 +154,14 @@
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="layers" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="layers"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">Batch Processing</h3> <h3 class="text-xl font-bold text-white">Batch Processing</h3>
</div> </div>
<p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p> <p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p>
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg"> <div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3"> <div class="flex items-center gap-4 mb-3">
<i <i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
data-lucide="zap"
class="w-10 h-10 text-indigo-400 flex-shrink-0"
></i>
<h3 class="text-xl font-bold text-white">Lightning Fast</h3> <h3 class="text-xl font-bold text-white">Lightning Fast</h3>
</div> </div>
<p class="text-gray-400 pl-14"> <p class="text-gray-400 pl-14">
@@ -235,47 +186,30 @@
<span class="absolute inset-y-0 left-0 flex items-center pl-3"> <span class="absolute inset-y-0 left-0 flex items-center pl-3">
<i data-lucide="search" class="w-5 h-5 text-gray-400"></i> <i data-lucide="search" class="w-5 h-5 text-gray-400"></i>
</span> </span>
<input <input type="text" id="search-bar"
type="text"
id="search-bar"
class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500" class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Search for a tool (e.g., 'split', 'organize'...)" placeholder="Search for a tool (e.g., 'split', 'organize'...)" />
/> <span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2">
<span
class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2"
>
<kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd> <kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd>
</span> </span>
</div> </div>
</div> </div>
<div <div id="tool-grid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6"></div>
id="tool-grid"
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6"
></div>
</div> </div>
<div <div id="tool-interface"
id="tool-interface" class="hidden w-full max-w-4xl mx-auto bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8 border border-gray-700">
class="hidden w-full max-w-4xl mx-auto bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8 border border-gray-700" <button id="back-to-grid"
> class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<button
id="back-to-grid"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
>
<i data-lucide="arrow-left" class="cursor-pointer"></i> <i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer"> Back to Tools </span> <span class="cursor-pointer"> Back to Tools </span>
</button> </button>
<div id="tool-content"></div> <div id="tool-content"></div>
</div> </div>
<div <div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
id="loader-modal" <div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50"
>
<div
class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl"
>
<div class="solid-spinner"></div> <div class="solid-spinner"></div>
<p id="loader-text" class="text-white text-lg font-medium"> <p id="loader-text" class="text-white text-lg font-medium">
Processing... Processing...
@@ -283,48 +217,33 @@
</div> </div>
</div> </div>
<div <div id="alert-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4">
id="alert-modal" <div class="bg-gray-800 max-w-sm w-full p-6 rounded-lg border border-gray-700 shadow-xl text-center">
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4"
>
<div
class="bg-gray-800 max-w-sm w-full p-6 rounded-lg border border-gray-700 shadow-xl text-center"
>
<h3 id="alert-title" class="text-xl font-bold text-white mb-2"> <h3 id="alert-title" class="text-xl font-bold text-white mb-2">
Alert Alert
</h3> </h3>
<p id="alert-message" class="text-gray-300 mb-6 whitespace-pre-line"> <p id="alert-message" class="text-gray-300 mb-6 whitespace-pre-line">
This is an alert message. This is an alert message.
</p> </p>
<button <button id="alert-ok"
id="alert-ok" class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700"
>
OK OK
</button> </button>
</div> </div>
</div> </div>
<div <div id="preview-modal"
id="preview-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40 p-4">
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40 p-4" <div class="bg-white text-black rounded-lg shadow-xl w-full max-w-4xl h-[90vh] flex flex-col">
>
<div
class="bg-white text-black rounded-lg shadow-xl w-full max-w-4xl h-[90vh] flex flex-col"
>
<div class="p-4 border-b flex justify-between items-center"> <div class="p-4 border-b flex justify-between items-center">
<h3 class="text-xl font-bold">Document Preview</h3> <h3 class="text-xl font-bold">Document Preview</h3>
<div class="flex gap-4"> <div class="flex gap-4">
<button <button id="preview-download-btn"
id="preview-download-btn" class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700"
>
Download as PDF Download as PDF
</button> </button>
<button <button id="preview-close-btn"
id="preview-close-btn" class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700">
class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700"
>
Close Close
</button> </button>
</div> </div>
@@ -338,96 +257,62 @@
<!-- COMPLIANCE SECTION START --> <!-- COMPLIANCE SECTION START -->
<section id="security-compliance-section" class="py-20 hide-section"> <section id="security-compliance-section" class="py-20 hide-section">
<div class="mb-8 text-center"> <div class="mb-8 text-center">
<h2 <h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance">
class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance"
>
Your data never leaves your device Your data never leaves your device
<span class="inline-flex items-center gap-2"> <span class="inline-flex items-center gap-2">
<i <i data-lucide="shield" class="w-8 h-8 md:w-10 md:h-10 text-indigo-400 bg-indigo-900 rounded-lg p-1.5"></i>
data-lucide="shield"
class="w-8 h-8 md:w-10 md:h-10 text-indigo-400 bg-indigo-900 rounded-lg p-1.5"
></i>
We keep We keep
</span> </span>
<br class="hidden sm:block" /> <br class="hidden sm:block" />
<span <span
class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold" class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold">
>
your information safe your information safe
</span> </span>
by following global security standards. by following global security standards.
</h2> </h2>
</div> </div>
<div class="mb-16 text-center"> <div class="mb-16 text-center">
<span <span class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors">
class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors"
>
All the processing happens locally on your device. All the processing happens locally on your device.
</span> </span>
</div> </div>
<div <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4">
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4"
>
<!-- GDPR Compliance --> <!-- GDPR Compliance -->
<div class="flex flex-col items-center text-center"> <div class="flex flex-col items-center text-center">
<div <div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4" <img src="/images/gdpr.svg" alt="GDPR compliance" class="w-full h-full" />
>
<img
src="/images/gdpr.svg"
alt="GDPR compliance"
class="w-full h-full"
/>
</div> </div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3"> <h3 class="text-lg md:text-xl font-bold text-white mb-3">
GDPR compliance GDPR compliance
</h3> </h3>
<p <p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
>
Protects the personal data and privacy of individuals within the Protects the personal data and privacy of individuals within the
European Union. European Union.
</p> </p>
</div> </div>
<div class="flex flex-col items-center text-center"> <div class="flex flex-col items-center text-center">
<div <div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4" <img src="/images/ccpa.svg" alt="CCPA compliance" class="w-full h-full" />
>
<img
src="/images/ccpa.svg"
alt="CCPA compliance"
class="w-full h-full"
/>
</div> </div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3"> <h3 class="text-lg md:text-xl font-bold text-white mb-3">
CCPA compliance CCPA compliance
</h3> </h3>
<p <p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
>
Gives California residents rights over how their personal Gives California residents rights over how their personal
information is collected, used, and shared. information is collected, used, and shared.
</p> </p>
</div> </div>
<div class="flex flex-col items-center text-center"> <div class="flex flex-col items-center text-center">
<div <div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4" <img src="/images/hipaa.svg" alt="HIPAA compliance" class="w-full h-full" />
>
<img
src="/images/hipaa.svg"
alt="HIPAA compliance"
class="w-full h-full"
/>
</div> </div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3"> <h3 class="text-lg md:text-xl font-bold text-white mb-3">
HIPAA compliance HIPAA compliance
</h3> </h3>
<p <p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
>
Sets safeguards for handling sensitive health information in the Sets safeguards for handling sensitive health information in the
United States healthcare system. United States healthcare system.
</p> </p>
@@ -440,27 +325,16 @@
<div class="section-divider hide-section"></div> <div class="section-divider hide-section"></div>
<section id="faq-accordion" class="space-y-4 hide-section"> <section id="faq-accordion" class="space-y-4 hide-section">
<h2 <h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8">
class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8"
>
Frequently Asked <span class="marker-slanted">Questions</span> Frequently Asked <span class="marker-slanted">Questions</span>
</h2> </h2>
<!-- Existing FAQs here... --> <!-- Existing FAQs here... -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Is BentoPDF really free?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Is BentoPDF really free?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Yes, absolutely. All tools on BentoPDF are 100% free to use, with Yes, absolutely. All tools on BentoPDF are 100% free to use, with
no file limits, no sign-ups, and no watermarks. We believe no file limits, no sign-ups, and no watermarks. We believe
@@ -471,20 +345,11 @@
</div> </div>
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Are my files secure? Where are they processed?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Are my files secure? Where are they processed?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Your files are as secure as possible because they **never leave Your files are as secure as possible because they **never leave
your computer**. All processing happens directly in your web your computer**. All processing happens directly in your web
@@ -495,20 +360,11 @@
</div> </div>
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Does it work on Mac, Windows, and Mobile?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Does it work on Mac, Windows, and Mobile?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Yes! Since BentoPDF runs entirely in your browser, it works on any Yes! Since BentoPDF runs entirely in your browser, it works on any
operating system with a modern web browser, including Windows, operating system with a modern web browser, including Windows,
@@ -519,20 +375,11 @@
<!-- New FAQ: GDPR Compliance --> <!-- New FAQ: GDPR Compliance -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Is BentoPDF GDPR compliant?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Is BentoPDF GDPR compliant?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Yes. BentoPDF is fully GDPR compliant. Since all file processing Yes. BentoPDF is fully GDPR compliant. Since all file processing
happens locally in your browser and we never collect or transmit happens locally in your browser and we never collect or transmit
@@ -544,20 +391,11 @@
<!-- New FAQ: Data Storage --> <!-- New FAQ: Data Storage -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Do you store or track any of my files?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Do you store or track any of my files?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
No. We never store, track, or log your files. Everything you do on No. We never store, track, or log your files. Everything you do on
BentoPDF happens in your browser memory and disappears once you BentoPDF happens in your browser memory and disappears once you
@@ -569,20 +407,11 @@
<!-- New FAQ: Privacy --> <!-- New FAQ: Privacy -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">What makes BentoPDF different from other PDF tools?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>What makes BentoPDF different from other PDF tools?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
Most PDF tools upload your files to a server for processing. Most PDF tools upload your files to a server for processing.
BentoPDF never does that. We use secure, modern web technology to BentoPDF never does that. We use secure, modern web technology to
@@ -594,20 +423,11 @@
<!-- New FAQ: Browser-Based --> <!-- New FAQ: Browser-Based -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">How does browser-based processing keep me safe?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>How does browser-based processing keep me safe?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
By running entirely inside your browser, BentoPDF ensures that By running entirely inside your browser, BentoPDF ensures that
your files never leave your device. This eliminates the risks of your files never leave your device. This eliminates the risks of
@@ -619,30 +439,16 @@
<!-- New FAQ: Analytics --> <!-- New FAQ: Analytics -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700"> <div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button <button class="faq-question w-full flex justify-between items-center text-left p-6">
class="faq-question w-full flex justify-between items-center text-left p-6" <span class="text-lg font-semibold text-white">Do you use cookies or analytics to track me?</span>
> <i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
<span class="text-lg font-semibold text-white"
>Do you use cookies or analytics to track me?</span
>
<i
data-lucide="chevron-down"
class="faq-icon w-6 h-6 text-gray-400 transition-transform"
></i>
</button> </button>
<div <div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
>
<p class="p-6 pt-0 text-gray-400"> <p class="p-6 pt-0 text-gray-400">
We care about your privacy. BentoPDF does not track personal We care about your privacy. BentoPDF does not track personal
information. We use information. We use
<a <a href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank"
href="https://simpleanalytics.com" rel="noopener noreferrer">Simple Analytics</a>
class="text-indigo-400 hover:underline"
target="_blank"
rel="noopener noreferrer"
>Simple Analytics</a
>
solely to see anonymous visit counts. This means we can know how solely to see anonymous visit counts. This means we can know how
many users visit our site, but many users visit our site, but
<strong>we never know who you are</strong>. Simple Analytics is <strong>we never know who you are</strong>. Simple Analytics is
@@ -658,9 +464,7 @@
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12"> <h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
What Our <span class="marker-slanted">Users</span> Say What Our <span class="marker-slanted">Users</span> Say
</h2> </h2>
<div <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto"
>
<div class="testimonial-card"> <div class="testimonial-card">
<div class="flex items-center mb-4"> <div class="flex items-center mb-4">
<div> <div>
@@ -757,9 +561,7 @@
<div class="section-divider"></div> <div class="section-divider"></div>
<section id="support-section" class="py-20"> <section id="support-section" class="py-20">
<div <div class="max-w-4xl mx-auto text-center bg-gray-800 p-8 md:p-12 rounded-xl border border-gray-700 shadow-2xl">
class="max-w-4xl mx-auto text-center bg-gray-800 p-8 md:p-12 rounded-xl border border-gray-700 shadow-2xl"
>
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4"> <h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Like My Work? Like My Work?
</h2> </h2>
@@ -769,12 +571,8 @@
supporting its development. Every coffee helps! supporting its development. Every coffee helps!
</p> </p>
<a <a href="https://ko-fi.com/alio01" target="_blank" rel="noopener noreferrer"
href="https://ko-fi.com/alio01" class="inline-flex items-center gap-3 px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5">
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-3 px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5"
>
<i data-lucide="coffee" class="w-7 h-7"></i> <i data-lucide="coffee" class="w-7 h-7"></i>
<span>Buy Me a Coffee</span> <span>Buy Me a Coffee</span>
</a> </a>
@@ -786,38 +584,31 @@
<footer class="mt-16 border-t-2 border-gray-700 py-8"> <footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div <div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
>
<div class="mb-8 md:mb-0"> <div class="mb-8 md:mb-0">
<div class="flex items-center justify-center md:justify-start mb-4"> <div class="flex items-center justify-center md:justify-start mb-4">
<img <img src="public/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
src="public/images/favicon.svg"
alt="Bento PDF Logo"
class="h-10 w-10 mr-3"
/>
<span class="text-xl font-bold text-white">BentoPDF</span> <span class="text-xl font-bold text-white">BentoPDF</span>
</div> </div>
<p class="text-gray-400 text-sm"> <p class="text-gray-400 text-sm">
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version">1.6.2</span>
</p>
</div> </div>
<div> <div>
<h3 class="font-bold text-white mb-4">Company</h3> <h3 class="font-bold text-white mb-4">Company</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="./about.html" class="hover:text-indigo-400" <a href="./about.html" class="hover:text-indigo-400">About Us</a>
>About Us</a
>
</li> </li>
<li> <li>
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a> <a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
</li> </li>
<li> <li>
<a href="./contact.html" class="hover:text-indigo-400" <a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
>Contact Us</a
>
</li> </li>
</ul> </ul>
</div> </div>
@@ -826,14 +617,10 @@
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="./terms.html" class="hover:text-indigo-400" <a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
>Terms and Conditions</a
>
</li> </li>
<li> <li>
<a href="./privacy.html" class="hover:text-indigo-400" <a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
>Privacy Policy</a
>
</li> </li>
</ul> </ul>
</div> </div>
@@ -841,72 +628,33 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Follow Us</h3> <h3 class="font-bold text-white mb-4">Follow Us</h3>
<div class="flex justify-center md:justify-start space-x-4"> <div class="flex justify-center md:justify-start space-x-4">
<a <a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
href="https://github.com/alam00000/bentopdf" class="text-gray-400 hover:text-indigo-400" title="GitHub">
target="_blank" <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
rel="noopener noreferrer" <path fill-rule="evenodd"
class="text-gray-400 hover:text-indigo-400"
title="GitHub"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" clip-rule="evenodd" />
/>
</svg> </svg>
</a> </a>
<a <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
href="https://discord.gg/q42xWQmJ" class="text-gray-400 hover:text-indigo-400" title="Discord">
target="_blank" <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400"
title="Discord"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path <path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
/>
</svg> </svg>
</a> </a>
<a <a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
href="https://www.instagram.com/thebentopdf/" title="Instagram">
class="text-gray-400 hover:text-indigo-400"
title="Instagram"
>
<i data-lucide="instagram"></i> <i data-lucide="instagram"></i>
</a> </a>
<a <a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
href="https://www.linkedin.com/company/bentopdf/" title="LinkedIn">
class="text-gray-400 hover:text-indigo-400"
title="LinkedIn"
>
<i data-lucide="linkedin"></i> <i data-lucide="linkedin"></i>
</a> </a>
<a <a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
href="https://x.com/BentoPDF" <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
class="text-gray-400 hover:text-indigo-400"
title="X (Twitter)"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path <path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
/>
</svg> </svg>
</a> </a>
</div> </div>
@@ -919,4 +667,5 @@
<script type="module" src="src/js/main.ts"></script> <script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"version": "1.2.0", "version": "1.6.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bento-pdf", "name": "bento-pdf",
"version": "1.2.0", "version": "1.6.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fontsource/cedarville-cursive": "^5.2.7", "@fontsource/cedarville-cursive": "^5.2.7",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"private": true, "private": true,
"version": "1.3.2", "version": "1.6.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
+42
View File
@@ -0,0 +1,42 @@
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
interface GetAttachmentsMessage {
command: 'get-attachments';
fileBuffer: ArrayBuffer;
fileName: string;
}
interface EditAttachmentsMessage {
command: 'edit-attachments';
fileBuffer: ArrayBuffer;
fileName: string;
attachmentsToRemove: number[];
}
type EditAttachmentsWorkerMessage = GetAttachmentsMessage | EditAttachmentsMessage;
interface GetAttachmentsSuccessResponse {
status: 'success';
attachments: Array<{ index: number; name: string; page: number; data: ArrayBuffer }>;
fileName: string;
}
interface GetAttachmentsErrorResponse {
status: 'error';
message: string;
}
interface EditAttachmentsSuccessResponse {
status: 'success';
modifiedPDF: ArrayBuffer;
fileName: string;
}
interface EditAttachmentsErrorResponse {
status: 'error';
message: string;
}
type GetAttachmentsResponse = GetAttachmentsSuccessResponse | GetAttachmentsErrorResponse;
type EditAttachmentsResponse = EditAttachmentsSuccessResponse | EditAttachmentsErrorResponse;
type EditAttachmentsWorkerResponse = GetAttachmentsResponse | EditAttachmentsResponse;
+151
View File
@@ -0,0 +1,151 @@
self.importScripts('/coherentpdf.browser.min.js');
function getAttachmentsFromPDFInWorker(fileBuffer, fileName) {
try {
const uint8Array = new Uint8Array(fileBuffer);
let pdf;
try {
pdf = coherentpdf.fromMemory(uint8Array, '');
} catch (error) {
self.postMessage({
status: 'error',
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`
});
return;
}
coherentpdf.startGetAttachments(pdf);
const attachmentCount = coherentpdf.numberGetAttachments();
if (attachmentCount === 0) {
self.postMessage({
status: 'success',
attachments: [],
fileName: fileName
});
coherentpdf.deletePdf(pdf);
return;
}
const attachments = [];
for (let i = 0; i < attachmentCount; i++) {
try {
const name = coherentpdf.getAttachmentName(i);
const page = coherentpdf.getAttachmentPage(i);
const attachmentData = coherentpdf.getAttachmentData(i);
const dataArray = new Uint8Array(attachmentData);
const buffer = dataArray.buffer.slice(dataArray.byteOffset, dataArray.byteOffset + dataArray.byteLength);
attachments.push({
index: i,
name: String(name),
page: Number(page),
data: buffer
});
} catch (error) {
console.warn(`Failed to get attachment ${i} from ${fileName}:`, error);
}
}
coherentpdf.endGetAttachments();
coherentpdf.deletePdf(pdf);
const response = {
status: 'success',
attachments: attachments,
fileName: fileName
};
const transferBuffers = attachments.map(att => att.data);
self.postMessage(response, transferBuffers);
} catch (error) {
self.postMessage({
status: 'error',
message: error instanceof Error
? error.message
: 'Unknown error occurred during attachment listing.'
});
}
}
function editAttachmentsInPDFInWorker(fileBuffer, fileName, attachmentsToRemove) {
try {
const uint8Array = new Uint8Array(fileBuffer);
let pdf;
try {
pdf = coherentpdf.fromMemory(uint8Array, '');
} catch (error) {
self.postMessage({
status: 'error',
message: `Failed to load PDF: ${fileName}. Error: ${error.message || error}`
});
return;
}
if (attachmentsToRemove && attachmentsToRemove.length > 0) {
coherentpdf.startGetAttachments(pdf);
const attachmentCount = coherentpdf.numberGetAttachments();
const attachmentsToKeep = [];
for (let i = 0; i < attachmentCount; i++) {
if (!attachmentsToRemove.includes(i)) {
const name = coherentpdf.getAttachmentName(i);
const page = coherentpdf.getAttachmentPage(i);
const data = coherentpdf.getAttachmentData(i);
const dataCopy = new Uint8Array(data.length);
dataCopy.set(new Uint8Array(data));
attachmentsToKeep.push({
name: String(name),
page: Number(page),
data: dataCopy
});
}
}
coherentpdf.endGetAttachments();
coherentpdf.removeAttachedFiles(pdf);
for (const attachment of attachmentsToKeep) {
if (attachment.page === 0) {
coherentpdf.attachFileFromMemory(attachment.data, attachment.name, pdf);
} else {
coherentpdf.attachFileToPageFromMemory(attachment.data, attachment.name, pdf, attachment.page);
}
}
}
const modifiedBytes = coherentpdf.toMemory(pdf, false, true);
coherentpdf.deletePdf(pdf);
const buffer = modifiedBytes.buffer.slice(modifiedBytes.byteOffset, modifiedBytes.byteOffset + modifiedBytes.byteLength);
const response = {
status: 'success',
modifiedPDF: buffer,
fileName: fileName
};
self.postMessage(response, [response.modifiedPDF]);
} catch (error) {
self.postMessage({
status: 'error',
message: error instanceof Error
? error.message
: 'Unknown error occurred during attachment editing.'
});
}
}
self.onmessage = (e) => {
if (e.data.command === 'get-attachments') {
getAttachmentsFromPDFInWorker(e.data.fileBuffer, e.data.fileName);
} else if (e.data.command === 'edit-attachments') {
editAttachmentsInPDFInWorker(e.data.fileBuffer, e.data.fileName, e.data.attachmentsToRemove);
}
};
+19
View File
@@ -0,0 +1,19 @@
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
interface ExtractAttachmentsMessage {
command: 'extract-attachments';
fileBuffers: ArrayBuffer[];
fileNames: string[];
}
interface ExtractAttachmentSuccessResponse {
status: 'success';
attachments: Array<{ name: string; data: ArrayBuffer }>;
}
interface ExtractAttachmentErrorResponse {
status: 'error';
message: string;
}
type ExtractAttachmentResponse = ExtractAttachmentSuccessResponse | ExtractAttachmentErrorResponse;
@@ -0,0 +1,106 @@
self.importScripts('/coherentpdf.browser.min.js');
function extractAttachmentsFromPDFsInWorker(fileBuffers, fileNames) {
try {
const allAttachments = [];
const totalFiles = fileBuffers.length;
for (let i = 0; i < totalFiles; i++) {
const buffer = fileBuffers[i];
const fileName = fileNames[i];
const uint8Array = new Uint8Array(buffer);
let pdf;
try {
pdf = coherentpdf.fromMemory(uint8Array, '');
} catch (error) {
console.warn(`Failed to load PDF: ${fileName}`, error);
continue;
}
coherentpdf.startGetAttachments(pdf);
const attachmentCount = coherentpdf.numberGetAttachments();
if (attachmentCount === 0) {
console.warn(`No attachments found in ${fileName}`);
coherentpdf.deletePdf(pdf);
continue;
}
const baseName = fileName.replace(/\.pdf$/i, '');
for (let j = 0; j < attachmentCount; j++) {
try {
const attachmentName = coherentpdf.getAttachmentName(j);
const attachmentPage = coherentpdf.getAttachmentPage(j);
const attachmentData = coherentpdf.getAttachmentData(j);
let uniqueName = attachmentName;
let counter = 1;
while (allAttachments.some(att => att.name === uniqueName)) {
const nameParts = attachmentName.split('.');
if (nameParts.length > 1) {
const extension = nameParts.pop();
uniqueName = `${nameParts.join('.')}_${counter}.${extension}`;
} else {
uniqueName = `${attachmentName}_${counter}`;
}
counter++;
}
if (attachmentPage > 0) {
uniqueName = `${baseName}_page${attachmentPage}_${uniqueName}`;
} else {
uniqueName = `${baseName}_${uniqueName}`;
}
allAttachments.push({
name: uniqueName,
data: attachmentData.buffer.slice(0)
});
} catch (error) {
console.warn(`Failed to extract attachment ${j} from ${fileName}:`, error);
}
}
coherentpdf.endGetAttachments();
coherentpdf.deletePdf(pdf);
}
if (allAttachments.length === 0) {
self.postMessage({
status: 'error',
message: 'No attachments were found in the selected PDF(s).'
});
return;
}
const response = {
status: 'success',
attachments: []
};
const transferBuffers = [];
for (const attachment of allAttachments) {
response.attachments.push({
name: attachment.name,
data: attachment.data
});
transferBuffers.push(attachment.data);
}
self.postMessage(response, transferBuffers);
} catch (error) {
self.postMessage({
status: 'error',
message: error instanceof Error
? error.message
: 'Unknown error occurred during attachment extraction.'
});
}
}
self.onmessage = (e) => {
if (e.data.command === 'extract-attachments') {
extractAttachmentsFromPDFsInWorker(e.data.fileBuffers, e.data.fileNames);
}
};
+11
View File
@@ -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;
}
+4 -1
View File
@@ -6,7 +6,6 @@ export const singlePdfLoadTools = [
'pdf-to-jpg', 'pdf-to-jpg',
'pdf-to-png', 'pdf-to-png',
'pdf-to-webp', 'pdf-to-webp',
'compress',
'pdf-to-greyscale', 'pdf-to-greyscale',
'edit-metadata', 'edit-metadata',
'remove-metadata', 'remove-metadata',
@@ -38,6 +37,7 @@ export const singlePdfLoadTools = [
'posterize', 'posterize',
'remove-blank-pages', 'remove-blank-pages',
'add-attachments', 'add-attachments',
'edit-attachments',
'sanitize-pdf', 'sanitize-pdf',
'remove-restrictions', 'remove-restrictions',
'bookmark-pdf', 'bookmark-pdf',
@@ -65,4 +65,7 @@ export const multiFileTools = [
'alternate-merge', 'alternate-merge',
'linearize', 'linearize',
'reverse-pages', 'reverse-pages',
'txt-to-pdf',
'compress',
'extract-attachments',
]; ];
+24
View File
@@ -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,6 +318,24 @@ 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.',
}, },
{
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 or remove attachments in your PDF.',
},
{
href: '/src/pages/pdf-multi-tool.html',
name: 'PDF Multi Tool',
icon: 'pencil-ruler',
subtitle: 'Full-featured PDF editor with page management.',
},
{ {
id: 'split', id: 'split',
name: 'Split PDF', name: 'Split PDF',
+82 -6
View File
@@ -504,29 +504,86 @@ async function handleMultiFileUpload(toolId) {
toolLogic['alternate-merge'].setup(); toolLogic['alternate-merge'].setup();
} else if (toolId === 'image-to-pdf') { } else if (toolId === 'image-to-pdf') {
const imageList = document.getElementById('image-list'); const imageList = document.getElementById('image-list');
imageList.textContent = ''; // Clear safely imageList.textContent = '';
state.files.forEach((file) => { state.files.forEach((file) => {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'relative group cursor-move'; li.className = 'relative group cursor-move';
li.dataset.fileName = file.name; li.dataset.fileName = file.name;
const wrapper = document.createElement('div');
wrapper.className = 'w-full h-36 sm:h-40 md:h-44 bg-gray-900 rounded-md border-2 border-gray-600 flex items-center justify-center overflow-hidden';
const img = document.createElement('img'); const img = document.createElement('img');
img.src = url; img.src = url;
img.className = img.className = 'max-w-full max-h-full object-contain';
'w-full h-full object-cover rounded-md border-2 border-gray-600';
const p = document.createElement('p'); const p = document.createElement('p');
p.className = p.className =
'absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1'; 'absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1';
p.textContent = file.name; // Safe insertion p.textContent = file.name;
li.append(img, p); wrapper.appendChild(img);
li.append(wrapper, p);
imageList.appendChild(li); imageList.appendChild(li);
}); });
Sortable.create(imageList); Sortable.create(imageList);
// Show image-to-pdf options and wire up slider text
const opts = document.getElementById('image-to-pdf-options');
if (opts) {
opts.classList.remove('hidden');
const slider = document.getElementById('image-pdf-quality') as HTMLInputElement;
const value = document.getElementById('image-pdf-quality-value');
if (slider && value) {
const update = () => (value.textContent = `${Math.round(parseFloat(slider.value) * 100)}%`);
slider.addEventListener('input', update);
update();
}
}
}
if (toolId === 'pdf-to-jpg') {
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
const qualityValue = document.getElementById('jpg-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
};
qualitySlider.addEventListener('input', updateValue);
updateValue();
}
}
if (toolId === 'pdf-to-png') {
const qualitySlider = document.getElementById('png-quality') as HTMLInputElement;
const qualityValue = document.getElementById('png-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
qualityValue.textContent = `${qualitySlider.value}x`;
};
qualitySlider.addEventListener('input', updateValue);
updateValue();
}
}
if (toolId === 'pdf-to-webp') {
const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement;
const qualityValue = document.getElementById('webp-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
};
qualitySlider.addEventListener('input', updateValue);
updateValue();
}
}
if (toolId === 'jpg-to-pdf' || toolId === 'png-to-pdf') {
const optionsDiv = document.getElementById(`${toolId}-options`);
if (optionsDiv) {
optionsDiv.classList.remove('hidden');
}
} }
} }
@@ -557,7 +614,26 @@ export function setupFileInputHandler(toolId) {
} }
if (isMultiFileTool) { if (isMultiFileTool) {
if (toolId === 'txt-to-pdf' || toolId === 'compress' || toolId === 'extract-attachments') {
const processBtn = document.getElementById('process-btn');
if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false;
if (toolId === 'compress') {
const optionsDiv = document.getElementById('compress-options');
if (optionsDiv) optionsDiv.classList.remove('hidden');
}
processBtn.onclick = () => {
const logic = toolLogic[toolId];
if (logic) {
const func =
typeof logic.process === 'function' ? logic.process : logic;
func();
}
};
}
} else {
await handleMultiFileUpload(toolId); await handleMultiFileUpload(toolId);
}
} else if (singlePdfLoadTools.includes(toolId)) { } else if (singlePdfLoadTools.includes(toolId)) {
await handleSinglePdfUpload(toolId, state.files[0]); await handleSinglePdfUpload(toolId, state.files[0]);
} else if (simpleTools.includes(toolId)) { } else if (simpleTools.includes(toolId)) {
+10 -1
View File
@@ -654,6 +654,9 @@ const fileDisplayArea = document.getElementById(
const backToToolsBtn = document.getElementById( const backToToolsBtn = document.getElementById(
'back-to-tools' 'back-to-tools'
) as HTMLButtonElement; ) as HTMLButtonElement;
const closeBtn = document.getElementById(
'back-btn'
) as HTMLButtonElement;
const canvas = document.getElementById('pdf-canvas'); const canvas = document.getElementById('pdf-canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const pageIndicator = document.getElementById('page-indicator'); const pageIndicator = document.getElementById('page-indicator');
@@ -2078,7 +2081,13 @@ async function extractExistingBookmarks(doc) {
// Back to tools button // Back to tools button
if (backToToolsBtn) { if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => { backToToolsBtn.addEventListener('click', () => {
window.location.href = '../../index.html#tools-header'; window.location.href = '/';
});
}
if (closeBtn) {
closeBtn.addEventListener('click', () => {
window.location.href = '/';
}); });
} }
+63
View File
@@ -272,6 +272,13 @@ export async function compress() {
const legacySettings = settings[level].legacy; const legacySettings = settings[level].legacy;
try { try {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
hideLoader();
return;
}
if (state.files.length === 1) {
const originalFile = state.files[0]; const originalFile = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(originalFile); const arrayBuffer = await readFileAsArrayBuffer(originalFile);
@@ -333,6 +340,62 @@ export async function compress() {
new Blob([resultBytes], { type: 'application/pdf' }), new Blob([resultBytes], { type: 'application/pdf' }),
'compressed-final.pdf' 'compressed-final.pdf'
); );
} else {
showLoader('Compressing multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
let totalOriginalSize = 0;
let totalCompressedSize = 0;
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Compressing ${i + 1}/${state.files.length}: ${file.name}...`);
const arrayBuffer = await readFileAsArrayBuffer(file);
totalOriginalSize += file.size;
let resultBytes;
if (algorithm === 'vector') {
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
} else if (algorithm === 'photon') {
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
} else {
const vectorResultBytes = await performSmartCompression(
arrayBuffer,
smartSettings
);
resultBytes = vectorResultBytes.length < file.size
? vectorResultBytes
: await performLegacyCompression(arrayBuffer, legacySettings);
}
totalCompressedSize += resultBytes.length;
const baseName = file.name.replace(/\.pdf$/i, '');
zip.file(`${baseName}_compressed.pdf`, resultBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
const totalSavings = totalOriginalSize - totalCompressedSize;
const totalSavingsPercent =
totalSavings > 0
? ((totalSavings / totalOriginalSize) * 100).toFixed(1)
: 0;
if (totalSavings > 0) {
showAlert(
'Compression Complete',
`Compressed ${state.files.length} PDF(s). ` +
`Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${totalSavingsPercent}%).`
);
} else {
showAlert(
'Compression Finished',
`Compressed ${state.files.length} PDF(s). ` +
`Total size: ${formatBytes(totalCompressedSize)}.`
);
}
downloadFile(zipBlob, 'compressed-pdfs.zip');
}
} catch (e) { } catch (e) {
showAlert( showAlert(
'Error', 'Error',
+218
View File
@@ -0,0 +1,218 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
const worker = new Worker('/workers/edit-attachments.worker.js');
let allAttachments: Array<{ index: number; name: string; page: number; data: Uint8Array }> = [];
let attachmentsToRemove: Set<number> = new Set();
export async function setupEditAttachmentsTool() {
const optionsDiv = document.getElementById('edit-attachments-options');
if (!optionsDiv || !state.files || state.files.length === 0) return;
optionsDiv.classList.remove('hidden');
await loadAttachmentsList();
}
async function loadAttachmentsList() {
const attachmentsList = document.getElementById('attachments-list');
if (!attachmentsList || !state.files || state.files.length === 0) return;
attachmentsList.innerHTML = '';
attachmentsToRemove.clear();
allAttachments = [];
try {
showLoader('Loading attachments...');
const file = state.files[0];
const fileBuffer = await readFileAsArrayBuffer(file);
const message = {
command: 'get-attachments',
fileBuffer: fileBuffer,
fileName: file.name
};
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error loading attachments:', error);
hideLoader();
showAlert('Error', 'Failed to load attachments from PDF.');
}
}
worker.onmessage = (e) => {
const data = e.data;
if (data.status === 'success' && data.attachments !== undefined) {
const attachments = data.attachments;
allAttachments = attachments.map(att => ({
...att,
data: new Uint8Array(att.data)
}));
displayAttachments(attachments);
hideLoader();
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`edited-attachments-${data.fileName}`
);
showAlert('Success', 'Attachments updated successfully!');
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
}
};
worker.onerror = (error) => {
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
};
function displayAttachments(attachments) {
const attachmentsList = document.getElementById('attachments-list');
if (!attachmentsList) return;
const existingControls = attachmentsList.querySelector('.attachments-controls');
attachmentsList.innerHTML = '';
if (existingControls) {
attachmentsList.appendChild(existingControls);
}
if (attachments.length === 0) {
const noAttachments = document.createElement('p');
noAttachments.className = 'text-gray-400 text-center py-4';
noAttachments.textContent = 'No attachments found in this PDF.';
attachmentsList.appendChild(noAttachments);
return;
}
const controlsContainer = document.createElement('div');
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
const removeAllBtn = document.createElement('button');
removeAllBtn.className = 'btn bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
removeAllBtn.textContent = 'Remove All Attachments';
removeAllBtn.onclick = () => {
if (allAttachments.length === 0) return;
const allSelected = allAttachments.every(attachment => attachmentsToRemove.has(attachment.index));
if (allSelected) {
allAttachments.forEach(attachment => {
attachmentsToRemove.delete(attachment.index);
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
if (element) {
element.classList.remove('opacity-50', 'line-through');
const removeBtn = element.querySelector('button');
if (removeBtn) {
removeBtn.classList.remove('bg-gray-600');
removeBtn.classList.add('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Remove All Attachments';
} else {
allAttachments.forEach(attachment => {
attachmentsToRemove.add(attachment.index);
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
if (element) {
element.classList.add('opacity-50', 'line-through');
const removeBtn = element.querySelector('button');
if (removeBtn) {
removeBtn.classList.add('bg-gray-600');
removeBtn.classList.remove('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Deselect All';
}
};
controlsContainer.appendChild(removeAllBtn);
attachmentsList.appendChild(controlsContainer);
for (const attachment of attachments) {
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 = attachment.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 = attachment.name;
const levelSpan = document.createElement('span');
levelSpan.className = 'text-gray-400 text-sm block';
if (attachment.page === 0) {
levelSpan.textContent = 'Document-level attachment';
} else {
levelSpan.textContent = `Page ${attachment.page} attachment`;
}
infoDiv.append(nameSpan, levelSpan);
const actionsDiv = document.createElement('div');
actionsDiv.className = 'flex items-center gap-2';
const removeBtn = document.createElement('button');
removeBtn.className = `btn ${attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.title = 'Remove attachment';
removeBtn.onclick = () => {
if (attachmentsToRemove.has(attachment.index)) {
attachmentsToRemove.delete(attachment.index);
attachmentDiv.classList.remove('opacity-50', 'line-through');
removeBtn.classList.remove('bg-gray-600');
removeBtn.classList.add('bg-red-600');
} else {
attachmentsToRemove.add(attachment.index);
attachmentDiv.classList.add('opacity-50', 'line-through');
removeBtn.classList.add('bg-gray-600');
removeBtn.classList.remove('bg-red-600');
}
const allSelected = allAttachments.every(attachment => attachmentsToRemove.has(attachment.index));
removeAllBtn.textContent = allSelected ? 'Deselect All' : 'Remove All Attachments';
};
actionsDiv.append(removeBtn);
attachmentDiv.append(infoDiv, actionsDiv);
attachmentsList.appendChild(attachmentDiv);
}
}
export async function editAttachments() {
if (!state.files || state.files.length === 0) {
showAlert('Error', 'No PDF file loaded.');
return;
}
showLoader('Processing attachments...');
try {
const file = state.files[0];
const fileBuffer = await readFileAsArrayBuffer(file);
const message = {
command: 'edit-attachments',
fileBuffer: fileBuffer,
fileName: file.name,
attachmentsToRemove: Array.from(attachmentsToRemove)
};
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error editing attachments:', error);
hideLoader();
showAlert('Error', 'Failed to edit attachments.');
}
}
+130
View File
@@ -0,0 +1,130 @@
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
const worker = new Worker('/workers/extract-attachments.worker.js');
interface ExtractAttachmentSuccessResponse {
status: 'success';
attachments: Array<{ name: string; data: ArrayBuffer }>;
}
interface ExtractAttachmentErrorResponse {
status: 'error';
message: string;
}
type ExtractAttachmentResponse = ExtractAttachmentSuccessResponse | ExtractAttachmentErrorResponse;
export async function extractAttachments() {
if (state.files.length === 0) {
showStatus('No Files', 'error');
return;
}
document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
document.getElementById('process-btn')?.setAttribute('disabled', 'true');
showStatus('Reading files (Main Thread)...', 'info');
try {
const fileBuffers: ArrayBuffer[] = [];
const fileNames: string[] = [];
for (const file of state.files) {
const buffer = await file.arrayBuffer();
fileBuffers.push(buffer);
fileNames.push(file.name);
}
showStatus(`Extracting attachments from ${state.files.length} file(s)...`, 'info');
const message: ExtractAttachmentsMessage = {
command: 'extract-attachments',
fileBuffers,
fileNames,
};
const transferables = fileBuffers.map(buf => buf);
worker.postMessage(message, transferables);
} catch (error) {
console.error('Error reading files:', error);
showStatus(
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
'error'
);
document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed');
document.getElementById('process-btn')?.removeAttribute('disabled');
}
}
worker.onmessage = (e: MessageEvent<ExtractAttachmentResponse>) => {
document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed');
document.getElementById('process-btn')?.removeAttribute('disabled');
if (e.data.status === 'success') {
const attachments = e.data.attachments;
const zip = new JSZip();
let totalSize = 0;
for (const attachment of attachments) {
zip.file(attachment.name, new Uint8Array(attachment.data));
totalSize += attachment.data.byteLength;
}
zip.generateAsync({ type: 'blob' }).then((zipBlob) => {
downloadFile(zipBlob, 'extracted-attachments.zip');
showStatus(
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
'success'
);
state.files = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
fileDisplayArea.classList.add('hidden');
}
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
document.getElementById('process-btn')?.setAttribute('disabled', 'true');
});
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
showStatus(`Error: ${errorMessage}`, 'error');
}
};
worker.onerror = (error) => {
console.error('Worker error:', error);
showStatus('Worker error occurred. Check console for details.', 'error');
document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed');
document.getElementById('process-btn')?.removeAttribute('disabled');
};
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
const statusMessage = document.getElementById('status-message') as HTMLElement;
if (!statusMessage) return;
statusMessage.textContent = message;
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
type === 'success'
? 'bg-green-900 text-green-200'
: type === 'error'
? 'bg-red-900 text-red-200'
: 'bg-blue-900 text-blue-200'
}`;
statusMessage.classList.remove('hidden');
}
interface ExtractAttachmentsMessage {
command: 'extract-attachments';
fileBuffers: ArrayBuffer[];
fileNames: string[];
}
+118 -100
View File
@@ -1,125 +1,139 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { jpgToPdf } from './jpg-to-pdf.js';
import { pngToPdf } from './png-to-pdf.js';
import { webpToPdf } from './webp-to-pdf.js';
import { bmpToPdf } from './bmp-to-pdf.js';
import { tiffToPdf } from './tiff-to-pdf.js';
import { svgToPdf } from './svg-to-pdf.js';
import { heicToPdf } from './heic-to-pdf.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
/**
* Converts any image into a standard, web-friendly JPEG. Loses transparency.
* @param {Uint8Array} imageBytes The raw bytes of the image file.
* @returns {Promise<Uint8Array>} A promise that resolves with sanitized JPEG bytes.
*/
function sanitizeImageAsJpeg(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (jpegBlob) => {
if (!jpegBlob)
return reject(new Error('Canvas to JPEG conversion failed.'));
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
},
'image/jpeg',
0.9
);
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(new Error('File could not be loaded as an image.'));
};
img.src = imageUrl;
});
}
/**
* Converts any image into a standard PNG. Preserves transparency.
* @param {Uint8Array} imageBytes The raw bytes of the image file.
* @returns {Promise<Uint8Array>} A promise that resolves with sanitized PNG bytes.
*/
function sanitizeImageAsPng(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(async (pngBlob) => {
if (!pngBlob)
return reject(new Error('Canvas to PNG conversion failed.'));
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
}, 'image/png');
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(new Error('File could not be loaded as an image.'));
};
img.src = imageUrl;
});
}
export async function imageToPdf() { export async function imageToPdf() {
if (state.files.length === 0) { if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one image file.'); showAlert('No Files', 'Please select at least one image file.');
return; return;
} }
const filesByType: { [key: string]: File[] } = {};
for (const file of state.files) {
const type = file.type || '';
if (!filesByType[type]) {
filesByType[type] = [];
}
filesByType[type].push(file);
}
const types = Object.keys(filesByType);
if (types.length === 1) {
const type = types[0];
const originalFiles = state.files;
if (type === 'image/jpeg' || type === 'image/jpg') {
state.files = filesByType[type] as File[];
await jpgToPdf();
} else if (type === 'image/png') {
state.files = filesByType[type] as File[];
await pngToPdf();
} else if (type === 'image/webp') {
state.files = filesByType[type] as File[];
await webpToPdf();
} else if (type === 'image/bmp') {
state.files = filesByType[type] as File[];
await bmpToPdf();
} else if (type === 'image/tiff' || type === 'image/tif') {
state.files = filesByType[type] as File[];
await tiffToPdf();
} else if (type === 'image/svg+xml') {
state.files = filesByType[type] as File[];
await svgToPdf();
} else {
const firstFile = filesByType[type][0];
if (firstFile.name.toLowerCase().endsWith('.heic') ||
firstFile.name.toLowerCase().endsWith('.heif')) {
state.files = filesByType[type] as File[];
await heicToPdf();
} else {
showLoader('Converting images to PDF...'); showLoader('Converting images to PDF...');
try { try {
const pdfDoc = await PDFLibDocument.create(); const pdfDoc = await PDFLibDocument.create();
for (const file of filesByType[type]) {
const imageBitmap = await createImageBitmap(file);
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
const pngBlob = await new Promise<Blob>((resolve) =>
canvas.toBlob(resolve, 'image/png')
);
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
imageBitmap.close();
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from-images.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert images to PDF.');
} finally {
hideLoader();
}
}
}
state.files = originalFiles;
return;
}
showLoader('Converting mixed image types to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
const imageList = document.getElementById('image-list'); const imageList = document.getElementById('image-list');
const sortedFiles = Array.from(imageList.children) const sortedFiles = imageList
? Array.from(imageList.children)
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
.map((li) => state.files.find((f) => f.name === li.dataset.fileName)) .map((li) => state.files.find((f) => f.name === li.dataset.fileName))
.filter(Boolean); .filter(Boolean)
: state.files;
const qualityInput = document.getElementById('image-pdf-quality') as HTMLInputElement;
const quality = qualityInput ? Math.max(0.3, Math.min(1.0, parseFloat(qualityInput.value))) : 0.9;
for (const file of sortedFiles) { for (const file of sortedFiles) {
const fileBuffer = await readFileAsArrayBuffer(file); const type = file.type || '';
let image; let image;
if (file.type === 'image/jpeg') {
try { try {
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array); const imageBitmap = await createImageBitmap(file);
} catch (e) { const canvas = document.createElement('canvas');
console.warn( canvas.width = imageBitmap.width;
`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...` canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
const jpegBlob = await new Promise<Blob>((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', quality)
); );
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer); const jpegBytes = new Uint8Array(await jpegBlob.arrayBuffer());
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array); image = await pdfDoc.embedJpg(jpegBytes);
} imageBitmap.close();
} else if (file.type === 'image/png') {
try {
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
} catch (e) {
console.warn(
`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`
);
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
}
} else {
// For WebP and other types, convert to PNG to preserve transparency
console.warn(
`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`
);
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
}
const page = pdfDoc.addPage([image.width, image.height]); const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, { page.drawImage(image, {
@@ -128,6 +142,10 @@ export async function imageToPdf() {
width: image.width, width: image.width,
height: image.height, height: image.height,
}); });
} catch (e) {
console.warn(`Failed to process ${file.name}:`, e);
// Continue with next file
}
} }
if (pdfDoc.getPageCount() === 0) { if (pdfDoc.getPageCount() === 0) {
+9 -2
View File
@@ -26,7 +26,7 @@ import { addHeaderFooter } from './add-header-footer.js';
import { imageToPdf } from './image-to-pdf.js'; import { imageToPdf } from './image-to-pdf.js';
import { changePermissions } from './change-permissions.js'; import { changePermissions } from './change-permissions.js';
import { pdfToMarkdown } from './pdf-to-markdown.js'; import { pdfToMarkdown } from './pdf-to-markdown.js';
import { txtToPdf } from './txt-to-pdf.js'; import { txtToPdf, setupTxtToPdfTool } from './txt-to-pdf.js';
import { invertColors } from './invert-colors.js'; import { invertColors } from './invert-colors.js';
// import { viewMetadata } from './view-metadata.js'; // import { viewMetadata } from './view-metadata.js';
import { reversePages } from './reverse-pages.js'; import { reversePages } from './reverse-pages.js';
@@ -63,6 +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 { 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';
@@ -96,7 +98,7 @@ export const toolLogic = {
'image-to-pdf': imageToPdf, 'image-to-pdf': imageToPdf,
'change-permissions': changePermissions, 'change-permissions': changePermissions,
'pdf-to-markdown': pdfToMarkdown, 'pdf-to-markdown': pdfToMarkdown,
'txt-to-pdf': txtToPdf, 'txt-to-pdf': { process: txtToPdf, setup: setupTxtToPdfTool },
'invert-colors': invertColors, 'invert-colors': invertColors,
'reverse-pages': reversePages, 'reverse-pages': reversePages,
// 'md-to-pdf': mdToPdf, // 'md-to-pdf': mdToPdf,
@@ -138,5 +140,10 @@ export const toolLogic = {
process: addAttachments, process: addAttachments,
setup: setupAddAttachmentsTool, setup: setupAddAttachmentsTool,
}, },
'extract-attachments': extractAttachments,
'edit-attachments': {
process: editAttachments,
setup: setupEditAttachmentsTool,
},
'sanitize-pdf': sanitizePdf, 'sanitize-pdf': sanitizePdf,
}; };
+2 -3
View File
@@ -16,8 +16,7 @@ function showStatus(
type: 'success' | 'error' | 'info' = 'info' type: 'success' | 'error' | 'info' = 'info'
) { ) {
statusMessage.textContent = message statusMessage.textContent = message
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${ statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
type === 'success'
? 'bg-green-900 text-green-200' ? 'bg-green-900 text-green-200'
: type === 'error' : type === 'error'
? 'bg-red-900 text-red-200' ? 'bg-red-900 text-red-200'
@@ -148,7 +147,7 @@ worker.onmessage = async (e: MessageEvent) => {
if (backToToolsBtn) { if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => { backToToolsBtn.addEventListener('click', () => {
window.location.href = '../../index.html#tools-header' window.location.href = '/'
}) })
} }
+902
View File
@@ -0,0 +1,902 @@
// @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',
import.meta.url
).toString();
interface PageData {
pdfIndex: number;
pageIndex: number;
rotation: number;
visualRotation: number;
canvas: HTMLCanvasElement;
pdfDoc: PDFLibDocument;
originalPageIndex: number;
}
let allPages: PageData[] = [];
let selectedPages: Set<number> = new Set();
let currentPdfDocs: PDFLibDocument[] = [];
let splitMarkers: Set<number> = new Set();
let isRendering = false;
let renderCancelled = false;
let sortableInstance: Sortable | null = null;
const pageCanvasCache = new Map<string, HTMLCanvasElement>();
type Snapshot = { allPages: PageData[]; selectedPages: number[]; splitMarkers: number[] };
const undoStack: Snapshot[] = [];
const redoStack: Snapshot[] = [];
function snapshot() {
const snap: Snapshot = {
allPages: allPages.map(p => ({ ...p, canvas: p.canvas })),
selectedPages: Array.from(selectedPages),
splitMarkers: Array.from(splitMarkers),
};
undoStack.push(snap);
redoStack.length = 0;
}
function restore(snap: Snapshot) {
allPages = snap.allPages.map(p => ({
...p,
canvas: p.canvas
}));
selectedPages = new Set(snap.selectedPages);
splitMarkers = new Set(snap.splitMarkers);
updatePageDisplay();
}
function showModal(title: string, message: string, type: 'info' | 'error' | 'success' = 'info') {
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modal-title');
const modalMessage = document.getElementById('modal-message');
const modalIcon = document.getElementById('modal-icon');
if (!modal || !modalTitle || !modalMessage || !modalIcon) return;
modalTitle.textContent = title;
modalMessage.textContent = message;
const iconMap = {
info: 'info',
error: 'alert-circle',
success: 'check-circle'
};
const colorMap = {
info: 'text-blue-400',
error: 'text-red-400',
success: 'text-green-400'
};
modalIcon.innerHTML = `<i data-lucide="${iconMap[type]}" class="w-12 h-12 ${colorMap[type]}"></i>`;
modal.classList.remove('hidden');
createIcons({ icons });
}
function hideModal() {
const modal = document.getElementById('modal');
if (modal) modal.classList.add('hidden');
}
function showLoading(current: number, total: number) {
const loader = document.getElementById('loading-overlay');
const progress = document.getElementById('loading-progress');
const text = document.getElementById('loading-text');
if (!loader || !progress || !text) return;
loader.classList.remove('hidden');
const percentage = Math.round((current / total) * 100);
progress.style.width = `${percentage}%`;
text.textContent = `Rendering pages... ${current} of ${total}`;
}
function hideLoading() {
const loader = document.getElementById('loading-overlay');
if (loader) loader.classList.add('hidden');
}
document.addEventListener('DOMContentLoaded', () => {
initializeTool();
});
function initializeTool() {
createIcons({ icons });
document.getElementById('close-tool-btn')?.addEventListener('click', () => {
window.location.href = '/';
});
document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => {
if (isRendering) {
showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info');
return;
}
document.getElementById('pdf-file-input')?.click();
});
document.getElementById('pdf-file-input')?.addEventListener('change', handlePdfUpload);
document.getElementById('insert-pdf-input')?.addEventListener('change', handleInsertPdf);
document.getElementById('bulk-rotate-left-btn')?.addEventListener('click', () => {
if (isRendering) return;
snapshot();
bulkRotate(-90);
});
document.getElementById('bulk-rotate-btn')?.addEventListener('click', () => {
if (isRendering) return;
snapshot();
bulkRotate(90);
});
document.getElementById('bulk-delete-btn')?.addEventListener('click', () => {
if (isRendering) return;
snapshot();
bulkDelete();
});
document.getElementById('bulk-duplicate-btn')?.addEventListener('click', () => {
if (isRendering) return;
snapshot();
bulkDuplicate();
});
document.getElementById('bulk-split-btn')?.addEventListener('click', () => {
if (isRendering) return;
snapshot();
bulkSplit();
});
document.getElementById('bulk-download-btn')?.addEventListener('click', () => {
if (isRendering) return;
bulkDownload();
});
document.getElementById('select-all-btn')?.addEventListener('click', () => {
if (isRendering) return;
selectAll();
});
document.getElementById('deselect-all-btn')?.addEventListener('click', () => {
if (isRendering) return;
deselectAll();
});
document.getElementById('export-pdf-btn')?.addEventListener('click', () => {
if (isRendering) return;
downloadAll();
});
document.getElementById('add-blank-page-btn')?.addEventListener('click', () => {
if (isRendering) return;
snapshot();
addBlankPage();
});
document.getElementById('undo-btn')?.addEventListener('click', () => {
if (isRendering) return;
const last = undoStack.pop();
if (last) {
const current: Snapshot = {
allPages: allPages.map(p => ({ ...p })),
selectedPages: Array.from(selectedPages),
splitMarkers: Array.from(splitMarkers),
};
redoStack.push(current);
restore(last);
}
});
document.getElementById('redo-btn')?.addEventListener('click', () => {
if (isRendering) return;
const next = redoStack.pop();
if (next) {
const current: Snapshot = {
allPages: allPages.map(p => ({ ...p })),
selectedPages: Array.from(selectedPages),
splitMarkers: Array.from(splitMarkers),
};
undoStack.push(current);
restore(next);
}
});
document.getElementById('reset-btn')?.addEventListener('click', () => {
if (isRendering) {
renderCancelled = true;
setTimeout(() => resetAll(), 100);
} else {
resetAll();
}
});
document.getElementById('modal-close-btn')?.addEventListener('click', hideModal);
document.getElementById('modal')?.addEventListener('click', (e) => {
if (e.target === document.getElementById('modal')) {
hideModal();
}
});
const uploadArea = document.getElementById('upload-area');
if (uploadArea) {
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('border-indigo-500');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('border-indigo-500');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('border-indigo-500');
const files = Array.from(e.dataTransfer?.files || []).filter(f => f.type === 'application/pdf');
if (files.length > 0) {
loadPdfs(files);
}
});
}
document.getElementById('upload-area')?.classList.remove('hidden');
}
function resetAll() {
snapshot();
allPages = [];
selectedPages.clear();
splitMarkers.clear();
currentPdfDocs = [];
pageCanvasCache.clear();
renderCancelled = false;
isRendering = false;
// Destroy sortable instance
if (sortableInstance) {
sortableInstance.destroy();
sortableInstance = null;
}
updatePageDisplay();
document.getElementById('upload-area')?.classList.remove('hidden');
}
async function handlePdfUpload(e: Event) {
const input = e.target as HTMLInputElement;
const files = Array.from(input.files || []);
if (files.length > 0) {
await loadPdfs(files);
}
input.value = '';
}
async function loadPdfs(files: File[]) {
if (isRendering) {
showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info');
return;
}
const uploadArea = document.getElementById('upload-area');
if (uploadArea) uploadArea.classList.add('hidden');
isRendering = true;
renderCancelled = false;
let totalPages = 0;
let currentPage = 0;
try {
// First pass: count total pages
const pdfDocs: PDFLibDocument[] = [];
for (const file of files) {
try {
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
pdfDocs.push(pdfDoc);
totalPages += pdfDoc.getPageCount();
} catch (e) {
console.error(`Failed to load PDF ${file.name}:`, e);
showModal('Error', `Failed to load ${file.name}. The file may be corrupted.`, 'error');
}
}
// Second pass: render pages
for (const pdfDoc of pdfDocs) {
if (renderCancelled) break;
currentPdfDocs.push(pdfDoc);
const numPages = pdfDoc.getPageCount();
for (let i = 0; i < numPages; i++) {
if (renderCancelled) break;
currentPage++;
showLoading(currentPage, totalPages);
await renderPage(pdfDoc, i, currentPdfDocs.length - 1);
}
}
if (!renderCancelled) {
setupSortable();
createIcons({ icons });
}
} finally {
hideLoading();
isRendering = false;
if (renderCancelled) {
renderCancelled = false;
}
}
}
function getCacheKey(pdfIndex: number, pageIndex: number): string {
return `${pdfIndex}-${pageIndex}`;
}
async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: number) {
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
// Check cache first
const cacheKey = getCacheKey(pdfIndex, pageIndex);
let canvas: HTMLCanvasElement;
if (pageCanvasCache.has(cacheKey)) {
canvas = pageCanvasCache.get(cacheKey)!;
} else {
const pdfBytes = await pdfDoc.save();
const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
const page = await pdf.getPage(pageIndex + 1);
const viewport = page.getViewport({ scale: 0.5, rotation: 0 });
canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
if (!context) return;
await page.render({
canvasContext: context,
viewport,
background: 'white',
canvas
}).promise;
// Cache the canvas
pageCanvasCache.set(cacheKey, canvas);
}
const pageData: PageData = {
pdfIndex,
pageIndex,
rotation: 0, // Actual rotation to apply when saving PDF
visualRotation: 0, // Visual rotation for display only
canvas,
pdfDoc,
originalPageIndex: pageIndex,
};
allPages.push(pageData);
createPageCard(pageData, allPages.length - 1);
}
function createPageCard(pageData: PageData, index: number) {
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
const card = document.createElement('div');
card.className = 'bg-gray-800 rounded-lg border-2 border-gray-700 p-2 relative group cursor-move';
card.dataset.pageIndex = index.toString();
if (selectedPages.has(index)) {
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
}
// Page preview
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.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)`;
previewCanvas.style.transition = 'transform 0.2s ease';
preview.appendChild(previewCanvas);
// Page info
const info = document.createElement('div');
info.className = 'text-xs text-gray-400 text-center mb-2';
info.textContent = `Page ${index + 1}`;
// 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 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');
selectBtn.className = 'absolute top-2 right-2 p-1 rounded bg-gray-900/70 hover:bg-gray-800 z-10';
selectBtn.innerHTML = selectedPages.has(index)
? '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>'
: '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>';
selectBtn.onclick = (e) => {
e.stopPropagation();
toggleSelectOptimized(index);
};
// Rotate button
const rotateBtn = document.createElement('button');
rotateBtn.className = 'p-1 rounded hover:bg-gray-700';
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4 text-gray-300"></i>';
rotateBtn.onclick = (e) => {
e.stopPropagation();
rotatePage(index, 90);
};
const rotateLeftBtn = document.createElement('button');
rotateLeftBtn.className = 'p-1 rounded hover:bg-gray-700';
rotateLeftBtn.innerHTML = '<i data-lucide="rotate-ccw" class="w-4 h-4 text-gray-300"></i>';
rotateLeftBtn.onclick = (e) => {
e.stopPropagation();
rotatePage(index, -90);
};
// Duplicate button
const duplicateBtn = document.createElement('button');
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.title = 'Duplicate this page';
duplicateBtn.onclick = (e) => {
e.stopPropagation();
snapshot();
duplicatePage(index);
};
// Delete button
const deleteBtn = document.createElement('button');
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.title = 'Delete this page';
deleteBtn.onclick = (e) => {
e.stopPropagation();
snapshot();
deletePage(index);
};
// Insert PDF button
const insertBtn = document.createElement('button');
insertBtn.className = 'p-1 rounded hover:bg-gray-700';
insertBtn.innerHTML = '<i data-lucide="file-plus" class="w-4 h-4 text-gray-300"></i>';
insertBtn.title = 'Insert PDF after this page';
insertBtn.onclick = (e) => {
e.stopPropagation();
snapshot();
insertPdfAfter(index);
};
// Split button
const splitBtn = document.createElement('button');
splitBtn.className = 'p-1 rounded hover:bg-gray-700';
splitBtn.innerHTML = '<i data-lucide="scissors" class="w-4 h-4 text-gray-300"></i>';
splitBtn.title = 'Toggle split after this page';
splitBtn.onclick = (e) => {
e.stopPropagation();
snapshot();
toggleSplitMarker(index);
renderSplitMarkers();
};
actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
card.append(preview, info, actions, selectBtn);
pagesContainer.appendChild(card);
createIcons({ icons });
}
function setupSortable() {
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
// Destroy existing instance before creating new one
if (sortableInstance) {
sortableInstance.destroy();
}
sortableInstance = Sortable.create(pagesContainer, {
animation: 150,
forceFallback: true,
touchStartThreshold: 3,
fallbackTolerance: 3,
delay: 200,
delayOnTouchOnly: true,
onEnd: (evt) => {
const oldIndex = evt.oldIndex!;
const newIndex = evt.newIndex!;
if (oldIndex !== newIndex) {
const [moved] = allPages.splice(oldIndex, 1);
allPages.splice(newIndex, 0, moved);
updatePageNumbers();
}
},
});
}
function toggleSelectOptimized(index: number) {
if (selectedPages.has(index)) {
selectedPages.delete(index);
} else {
selectedPages.add(index);
}
// Only update the specific card instead of re-rendering everything
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
const card = pagesContainer.children[index] as HTMLElement;
if (!card) return;
const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]');
if (!selectBtn) return;
if (selectedPages.has(index)) {
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
selectBtn.innerHTML = '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>';
} else {
card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500');
selectBtn.innerHTML = '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>';
}
createIcons({ icons });
}
function selectAll() {
selectedPages.clear();
allPages.forEach((_, index) => selectedPages.add(index));
updatePageDisplay();
}
function deselectAll() {
selectedPages.clear();
updatePageDisplay();
}
function rotatePage(index: number, delta: number) {
snapshot();
const pageData = allPages[index];
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
pageData.rotation = (pageData.rotation + delta + 360) % 360;
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
const card = pagesContainer.children[index] as HTMLElement;
if (!card) return;
const canvas = card.querySelector('canvas');
const preview = card.querySelector('.bg-white');
if (canvas && preview) {
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
canvas.style.transition = 'transform 0.2s ease';
}
}
function duplicatePage(index: number) {
const originalPageData = allPages[index];
const originalCanvas = originalPageData.canvas;
const newCanvas = document.createElement('canvas');
newCanvas.width = originalCanvas.width;
newCanvas.height = originalCanvas.height;
const newContext = newCanvas.getContext('2d');
if (newContext) {
newContext.drawImage(originalCanvas, 0, 0);
}
const newPageData: PageData = {
...originalPageData,
canvas: newCanvas,
};
const newIndex = index + 1;
allPages.splice(newIndex, 0, newPageData);
updatePageDisplay();
}
function deletePage(index: number) {
allPages.splice(index, 1);
selectedPages.delete(index);
const newSelected = new Set<number>();
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();
}
async function insertPdfAfter(index: number) {
document.getElementById('insert-pdf-input')?.click();
(window as any).__insertAfterIndex = index;
}
async function handleInsertPdf(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
const insertAfterIndex = (window as any).__insertAfterIndex;
if (insertAfterIndex === undefined) return;
try {
const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
currentPdfDocs.push(pdfDoc);
const numPages = pdfDoc.getPageCount();
const newPages: PageData[] = [];
for (let i = 0; i < numPages; i++) {
// Use the existing renderPage function, which adds to allPages
await renderPage(pdfDoc, i, currentPdfDocs.length - 1);
// Move the newly added page data to the temporary array
newPages.push(allPages.pop()!);
}
allPages.splice(insertAfterIndex + 1, 0, ...newPages);
updatePageDisplay();
} catch (e) {
console.error('Failed to insert PDF:', e);
showModal('Error', 'Failed to insert PDF. The file may be corrupted.', 'error');
}
input.value = '';
}
function toggleSplitMarker(index: number) {
if (splitMarkers.has(index)) splitMarkers.delete(index);
else splitMarkers.add(index);
}
function renderSplitMarkers() {
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove());
Array.from(pagesContainer.children).forEach((cardEl, i) => {
if (splitMarkers.has(i)) {
const marker = document.createElement('div');
marker.className = 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none';
marker.innerHTML = '<div class="h-full w-0.5 border-l-2 border-dashed border-blue-400"></div>';
(cardEl as HTMLElement).appendChild(marker);
}
});
}
function addBlankPage() {
const canvas = document.createElement('canvas');
canvas.width = 595;
canvas.height = 842;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, 595, 842);
}
const blankPageData: PageData = {
pdfIndex: -1,
pageIndex: -1,
rotation: 0,
visualRotation: 0,
canvas,
pdfDoc: null as any,
originalPageIndex: -1,
};
allPages.push(blankPageData);
updatePageDisplay();
}
function bulkRotate(delta: number) {
if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to rotate.', 'info');
return;
}
selectedPages.forEach(index => {
const pageData = allPages[index];
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
pageData.rotation = (pageData.rotation + delta + 360) % 360;
});
updatePageDisplay();
}
function bulkDelete() {
if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to delete.', 'info');
return;
}
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();
}
function bulkDuplicate() {
if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to duplicate.', 'info');
return;
}
const indices = Array.from(selectedPages).sort((a, b) => b - a);
indices.forEach(index => {
duplicatePage(index);
});
selectedPages.clear();
updatePageDisplay();
}
function bulkSplit() {
if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to mark for splitting.', 'info');
return;
}
const indices = Array.from(selectedPages);
indices.forEach(index => {
if (!splitMarkers.has(index)) {
splitMarkers.add(index);
}
});
renderSplitMarkers();
selectedPages.clear();
updatePageDisplay();
}
async function bulkDownload() {
if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to download.', 'info');
return;
}
const indices = Array.from(selectedPages);
await downloadPagesAsPdf(indices, 'selected-pages.pdf');
}
async function downloadAll() {
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) {
try {
const newPdf = await PDFLibDocument.create();
for (const index of indices) {
const pageData = allPages[index];
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
// Copy page from original PDF
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();
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
downloadFile(blob, filename);
} catch (e) {
console.error('Failed to create PDF:', e);
showModal('Error', 'Failed to create PDF.', 'error');
}
}
function updatePageDisplay() {
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
pagesContainer.innerHTML = '';
allPages.forEach((pageData, index) => {
createPageCard(pageData, index);
});
setupSortable();
renderSplitMarkers();
createIcons({ icons });
}
function updatePageNumbers() {
updatePageDisplay();
}
+4 -1
View File
@@ -12,6 +12,9 @@ export async function pdfToJpg() {
).promise; ).promise;
const zip = new JSZip(); const zip = new JSZip();
const qualityInput = document.getElementById('jpg-quality') as HTMLInputElement;
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9;
for (let i = 1; i <= pdf.numPages; i++) { for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i); const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 }); const viewport = page.getViewport({ scale: 2.0 });
@@ -23,7 +26,7 @@ export async function pdfToJpg() {
await page.render({ canvasContext: context, viewport: viewport }).promise; await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise((resolve) => const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', 0.9) canvas.toBlob(resolve, 'image/jpeg', quality)
); );
zip.file(`page_${i}.jpg`, blob as Blob); zip.file(`page_${i}.jpg`, blob as Blob);
} }
+2 -3
View File
@@ -16,8 +16,7 @@ function showStatus(
type: 'success' | 'error' | 'info' = 'info' type: 'success' | 'error' | 'info' = 'info'
) { ) {
statusMessage.textContent = message statusMessage.textContent = message
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${ statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
type === 'success'
? 'bg-green-900 text-green-200' ? 'bg-green-900 text-green-200'
: type === 'error' : type === 'error'
? 'bg-red-900 text-red-200' ? 'bg-red-900 text-red-200'
@@ -144,7 +143,7 @@ worker.onmessage = async (e: MessageEvent) => {
if (backToToolsBtn) { if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => { backToToolsBtn.addEventListener('click', () => {
window.location.href = '../../index.html#tools-header' window.location.href = '/'
}) })
} }
+5 -1
View File
@@ -11,9 +11,13 @@ export async function pdfToPng() {
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
const zip = new JSZip(); const zip = new JSZip();
const qualityInput = document.getElementById('png-quality') as HTMLInputElement;
const scale = qualityInput ? parseFloat(qualityInput.value) : 2.0;
for (let i = 1; i <= pdf.numPages; i++) { for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i); const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 }); const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; canvas.width = viewport.width;
+4 -1
View File
@@ -19,8 +19,11 @@ export async function pdfToWebp() {
canvas.width = viewport.width; canvas.width = viewport.width;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise; await page.render({ canvasContext: context, viewport: viewport }).promise;
const qualityInput = document.getElementById('webp-quality') as HTMLInputElement;
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9;
const blob = await new Promise((resolve) => const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/webp', 0.9) canvas.toBlob(resolve, 'image/webp', quality)
); );
zip.file(`page_${i}.webp`, blob as Blob); zip.file(`page_${i}.webp`, blob as Blob);
} }
+106 -2
View File
@@ -5,7 +5,6 @@ import JSZip from 'jszip';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
// Track if visual selector has been rendered to avoid duplicates
let visualSelectorRendered = false; let visualSelectorRendered = false;
async function renderVisualSelector() { async function renderVisualSelector() {
@@ -89,6 +88,9 @@ export function setupSplitTool() {
const evenOddPanel = document.getElementById('even-odd-panel'); const evenOddPanel = document.getElementById('even-odd-panel');
const zipOptionWrapper = document.getElementById('zip-option-wrapper'); const zipOptionWrapper = document.getElementById('zip-option-wrapper');
const allPagesPanel = document.getElementById('all-pages-panel'); const allPagesPanel = document.getElementById('all-pages-panel');
const bookmarksPanel = document.getElementById('bookmarks-panel');
const nTimesPanel = document.getElementById('n-times-panel');
const nTimesWarning = document.getElementById('n-times-warning');
if (!splitModeSelect) return; if (!splitModeSelect) return;
@@ -106,7 +108,10 @@ export function setupSplitTool() {
visualPanel.classList.add('hidden'); visualPanel.classList.add('hidden');
evenOddPanel.classList.add('hidden'); evenOddPanel.classList.add('hidden');
allPagesPanel.classList.add('hidden'); allPagesPanel.classList.add('hidden');
bookmarksPanel.classList.add('hidden');
nTimesPanel.classList.add('hidden');
zipOptionWrapper.classList.add('hidden'); zipOptionWrapper.classList.add('hidden');
if (nTimesWarning) nTimesWarning.classList.add('hidden');
if (mode === 'range') { if (mode === 'range') {
rangePanel.classList.remove('hidden'); rangePanel.classList.remove('hidden');
@@ -119,6 +124,34 @@ export function setupSplitTool() {
evenOddPanel.classList.remove('hidden'); evenOddPanel.classList.remove('hidden');
} else if (mode === 'all') { } else if (mode === 'all') {
allPagesPanel.classList.remove('hidden'); allPagesPanel.classList.remove('hidden');
} else if (mode === 'bookmarks') {
bookmarksPanel.classList.remove('hidden');
zipOptionWrapper.classList.remove('hidden');
} else if (mode === 'n-times') {
nTimesPanel.classList.remove('hidden');
zipOptionWrapper.classList.remove('hidden');
const updateWarning = () => {
if (!state.pdfDoc) return;
const totalPages = state.pdfDoc.getPageCount();
const nValue = parseInt((document.getElementById('split-n-value') as HTMLInputElement)?.value || '5');
const remainder = totalPages % nValue;
if (remainder !== 0 && nTimesWarning) {
nTimesWarning.classList.remove('hidden');
const warningText = document.getElementById('n-times-warning-text');
if (warningText) {
warningText.textContent = `The PDF has ${totalPages} pages, which is not evenly divisible by ${nValue}. The last PDF will contain ${remainder} page(s).`;
}
} else if (nTimesWarning) {
nTimesWarning.classList.add('hidden');
}
};
const nValueInput = document.getElementById('split-n-value') as HTMLInputElement;
if (nValueInput) {
nValueInput.addEventListener('input', updateWarning);
updateWarning();
}
} }
}); });
} }
@@ -185,10 +218,81 @@ export async function split() {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
.map((el) => parseInt(el.dataset.pageIndex)); .map((el) => parseInt(el.dataset.pageIndex));
break; break;
case 'bookmarks':
const { getCpdf } = await import('../utils/cpdf-helper.js');
const cpdf = await getCpdf();
const pdfBytes = await state.pdfDoc.save();
const pdf = cpdf.fromMemory(new Uint8Array(pdfBytes), '');
cpdf.startGetBookmarkInfo(pdf);
const bookmarkCount = cpdf.numberBookmarks();
const bookmarkLevel = (document.getElementById('bookmark-level') as HTMLSelectElement)?.value;
const splitPages: number[] = [];
for (let i = 0; i < bookmarkCount; i++) {
const level = cpdf.getBookmarkLevel(i);
const page = cpdf.getBookmarkPage(pdf, i);
if (bookmarkLevel === 'all' || level === parseInt(bookmarkLevel)) {
if (page > 1 && !splitPages.includes(page - 1)) {
splitPages.push(page - 1); // Convert to 0-based index
}
}
}
cpdf.endGetBookmarkInfo();
cpdf.deletePdf(pdf);
if (splitPages.length === 0) {
throw new Error('No bookmarks found at the selected level.');
}
splitPages.sort((a, b) => a - b);
const zip = new JSZip();
for (let i = 0; i < splitPages.length; i++) {
const startPage = i === 0 ? 0 : splitPages[i];
const endPage = i < splitPages.length - 1 ? splitPages[i + 1] - 1 : totalPages - 1;
const newPdf = await PDFLibDocument.create();
const pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, idx) => startPage + idx);
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const pdfBytes2 = await newPdf.save();
zip.file(`split-${i + 1}.pdf`, pdfBytes2);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'split-by-bookmarks.zip');
hideLoader();
return;
case 'n-times':
const nValue = parseInt((document.getElementById('split-n-value') as HTMLInputElement)?.value || '5');
if (nValue < 1) throw new Error('N must be at least 1.');
const zip2 = new JSZip();
const numSplits = Math.ceil(totalPages / nValue);
for (let i = 0; i < numSplits; i++) {
const startPage = i * nValue;
const endPage = Math.min(startPage + nValue - 1, totalPages - 1);
const pageIndices = Array.from({ length: endPage - startPage + 1 }, (_, idx) => startPage + idx);
const newPdf = await PDFLibDocument.create();
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const pdfBytes3 = await newPdf.save();
zip2.file(`split-${i + 1}.pdf`, pdfBytes3);
}
const zipBlob2 = await zip2.generateAsync({ type: 'blob' });
downloadFile(zipBlob2, 'split-n-times.zip');
hideLoader();
return;
} }
const uniqueIndices = [...new Set(indicesToExtract)]; const uniqueIndices = [...new Set(indicesToExtract)];
if (uniqueIndices.length === 0) { if (uniqueIndices.length === 0 && splitMode !== 'bookmarks' && splitMode !== 'n-times') {
throw new Error('No pages were selected for splitting.'); throw new Error('No pages were selected for splitting.');
} }
+2 -3
View File
@@ -53,8 +53,7 @@ function showStatus(
type: 'success' | 'error' | 'info' = 'info' type: 'success' | 'error' | 'info' = 'info'
) { ) {
statusMessage.textContent = message; statusMessage.textContent = message;
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${ statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
type === 'success'
? 'bg-green-900 text-green-200' ? 'bg-green-900 text-green-200'
: type === 'error' : type === 'error'
? 'bg-red-900 text-red-200' ? 'bg-red-900 text-red-200'
@@ -198,7 +197,7 @@ worker.onerror = (error) => {
if (backToToolsBtn) { if (backToToolsBtn) {
backToToolsBtn.addEventListener('click', () => { backToToolsBtn.addEventListener('click', () => {
window.location.href = '../../index.html#tools-header'; window.location.href = '/';
}); });
} }
+163 -21
View File
@@ -1,5 +1,7 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js'; import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import { import {
PDFDocument as PDFLibDocument, PDFDocument as PDFLibDocument,
@@ -8,31 +10,71 @@ import {
PageSizes, PageSizes,
} from 'pdf-lib'; } from 'pdf-lib';
export async function txtToPdf() { function sanitizeTextForPdf(text: string): string {
showLoader('Creating PDF...'); return text
try { .split('')
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message .map((char) => {
const text = document.getElementById('text-input').value; const code = char.charCodeAt(0);
if (!text.trim()) {
showAlert('Input Required', 'Please enter some text to convert.'); if (code === 0x20 || code === 0x09 || code === 0x0A) {
hideLoader(); return char;
return;
} }
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message if ((code >= 0x00 && code <= 0x1F) || (code >= 0x7F && code <= 0x9F)) {
const fontFamilyKey = document.getElementById('font-family').value; return ' ';
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message }
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message if (code < 0x20 || (code > 0x7E && code < 0xA0)) {
const pageSizeKey = document.getElementById('page-size').value; return ' ';
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message }
const colorHex = document.getElementById('text-color').value;
const textColor = hexToRgb(colorHex); const replacements: { [key: number]: string } = {
0x2018: "'",
0x2019: "'",
0x201C: '"',
0x201D: '"',
0x2013: '-',
0x2014: '--',
0x2026: '...',
0x00A0: ' ',
};
if (replacements[code]) {
return replacements[code];
}
try {
if (code <= 0xFF) {
return char;
}
return '?';
} catch {
return '?';
}
})
.join('')
.replace(/[ \t]+/g, ' ')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.split('\n')
.map((line) => line.trimEnd())
.join('\n');
}
async function createPdfFromText(
text: string,
fontFamilyKey: string,
fontSize: number,
pageSizeKey: string,
colorHex: string
): Promise<Uint8Array> {
const sanitizedText = sanitizeTextForPdf(text);
const pdfDoc = await PDFLibDocument.create(); const pdfDoc = await PDFLibDocument.create();
const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]); const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]);
const pageSize = PageSizes[pageSizeKey]; const pageSize = PageSizes[pageSizeKey];
const margin = 72; // 1 inch const margin = 72;
const textColor = hexToRgb(colorHex);
let page = pdfDoc.addPage(pageSize); let page = pdfDoc.addPage(pageSize);
let { width, height } = page.getSize(); let { width, height } = page.getSize();
@@ -40,7 +82,7 @@ export async function txtToPdf() {
const lineHeight = fontSize * 1.3; const lineHeight = fontSize * 1.3;
let y = height - margin; let y = height - margin;
const paragraphs = text.split('\n'); const paragraphs = sanitizedText.split('\n');
for (const paragraph of paragraphs) { for (const paragraph of paragraphs) {
const words = paragraph.split(' '); const words = paragraph.split(' ');
let currentLine = ''; let currentLine = '';
@@ -81,11 +123,111 @@ export async function txtToPdf() {
} }
} }
const pdfBytes = await pdfDoc.save(); return await pdfDoc.save();
}
export async function setupTxtToPdfTool() {
const uploadBtn = document.getElementById('txt-mode-upload-btn');
const textBtn = document.getElementById('txt-mode-text-btn');
const uploadPanel = document.getElementById('txt-upload-panel');
const textPanel = document.getElementById('txt-text-panel');
if (!uploadBtn || !textBtn || !uploadPanel || !textPanel) return;
const switchToUpload = () => {
uploadPanel.classList.remove('hidden');
textPanel.classList.add('hidden');
uploadBtn.classList.add('bg-indigo-600', 'text-white');
uploadBtn.classList.remove('bg-gray-700', 'text-gray-300');
textBtn.classList.remove('bg-indigo-600', 'text-white');
textBtn.classList.add('bg-gray-700', 'text-gray-300');
};
const switchToText = () => {
uploadPanel.classList.add('hidden');
textPanel.classList.remove('hidden');
textBtn.classList.add('bg-indigo-600', 'text-white');
textBtn.classList.remove('bg-gray-700', 'text-gray-300');
uploadBtn.classList.remove('bg-indigo-600', 'text-white');
uploadBtn.classList.add('bg-gray-700', 'text-gray-300');
};
uploadBtn.addEventListener('click', switchToUpload);
textBtn.addEventListener('click', switchToText);
}
export async function txtToPdf() {
const uploadPanel = document.getElementById('txt-upload-panel');
const isUploadMode = !uploadPanel?.classList.contains('hidden');
showLoader('Creating PDF...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontFamilyKey = document.getElementById('font-family').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageSizeKey = document.getElementById('page-size').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color').value;
if (isUploadMode && state.files.length > 0) {
if (state.files.length === 1) {
const file = state.files[0];
const text = await file.text();
const pdfBytes = await createPdfFromText(
text,
fontFamilyKey,
fontSize,
pageSizeKey,
colorHex
);
const baseName = file.name.replace(/\.txt$/i, '');
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
`${baseName}.pdf`
);
} else {
showLoader('Creating PDFs and ZIP archive...');
const zip = new JSZip();
for (const file of state.files) {
const text = await file.text();
const pdfBytes = await createPdfFromText(
text,
fontFamilyKey,
fontSize,
pageSizeKey,
colorHex
);
const baseName = file.name.replace(/\.txt$/i, '');
zip.file(`${baseName}.pdf`, pdfBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'text-to-pdf.zip');
}
} else {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const text = document.getElementById('text-input').value;
if (!text.trim()) {
showAlert('Input Required', 'Please enter some text to convert.');
hideLoader();
return;
}
const pdfBytes = await createPdfFromText(
text,
fontFamilyKey,
fontSize,
pageSizeKey,
colorHex
);
downloadFile( downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'text-document.pdf' 'text-document.pdf'
); );
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'Failed to create PDF from text.'); showAlert('Error', 'Failed to create PDF from text.');
+23
View File
@@ -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(
@@ -42,6 +43,11 @@ const init = () => {
heroSection.style.display = 'none'; heroSection.style.display = 'none';
} }
const githubLink = document.querySelector('a[href*="github.com/alam00000/bentopdf"]');
if (githubLink) {
(githubLink as HTMLElement).style.display = 'none';
}
const featuresSection = document.getElementById('features-section'); const featuresSection = document.getElementById('features-section');
if (featuresSection) { if (featuresSection) {
featuresSection.style.display = 'none'; featuresSection.style.display = 'none';
@@ -87,6 +93,9 @@ const init = () => {
<p class="text-gray-400 text-sm"> <p class="text-gray-400 text-sm">
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version-simple">1.6.2</span>
</p>
</div> </div>
`; `;
document.body.appendChild(simpleFooter); document.body.appendChild(simpleFooter);
@@ -259,6 +268,20 @@ const init = () => {
createIcons({ icons }); createIcons({ icons });
console.log('Please share our tool and share the love!'); console.log('Please share our tool and share the love!');
const githubStarsElement = document.getElementById('github-stars');
if (githubStarsElement && !__SIMPLE_MODE__) {
fetch('https://api.github.com/repos/alam00000/bentopdf')
.then((response) => response.json())
.then((data) => {
if (data.stargazers_count !== undefined) {
githubStarsElement.textContent = formatStars(data.stargazers_count);
}
})
.catch(() => {
githubStarsElement.textContent = '-';
});
}
}; };
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);
+136 -11
View File
@@ -321,6 +321,8 @@ export const toolTemplates = {
<option value="even-odd">Split by Even/Odd Pages</option> <option value="even-odd">Split by Even/Odd Pages</option>
<option value="all">Split All Pages into Separate Files</option> <option value="all">Split All Pages into Separate Files</option>
<option value="visual">Select Pages Visually</option> <option value="visual">Select Pages Visually</option>
<option value="bookmarks">Split by Bookmarks</option>
<option value="n-times">Split N Times</option>
</select> </select>
<div id="range-panel"> <div id="range-panel">
@@ -367,6 +369,39 @@ export const toolTemplates = {
<p class="text-xs text-gray-400 mt-1">This mode will create a separate PDF file for every single page in your document and download them together in one ZIP archive.</p> <p class="text-xs text-gray-400 mt-1">This mode will create a separate PDF file for every single page in your document and download them together in one ZIP archive.</p>
</div> </div>
<div id="bookmarks-panel" class="hidden">
<div class="p-3 bg-gray-900 rounded-lg border border-gray-700 mb-3">
<p class="text-sm text-gray-300"><strong class="text-white">How it works:</strong></p>
<p class="text-xs text-gray-400 mt-1">Split the PDF at bookmark locations. Each bookmark will start a new PDF file.</p>
</div>
<div class="mb-4">
<label for="bookmark-level" class="block mb-2 text-sm font-medium text-gray-300">Bookmark Level</label>
<select id="bookmark-level" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="0">Level 0 (Top level only)</option>
<option value="1">Level 1</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
<option value="all" selected>All Levels</option>
</select>
<p class="mt-1 text-xs text-gray-400">Select which bookmark nesting level to use for splitting</p>
</div>
</div>
<div id="n-times-panel" class="hidden">
<div class="p-3 bg-gray-900 rounded-lg border border-gray-700 mb-3">
<p class="text-sm text-gray-300"><strong class="text-white">How it works:</strong></p>
<p class="text-xs text-gray-400 mt-1">Split the PDF into N equal parts. For example, a 40-page PDF with N=5 will create 8 PDFs with 5 pages each.</p>
</div>
<div class="mb-4">
<label for="split-n-value" class="block mb-2 text-sm font-medium text-gray-300">Number of Pages per Split (N)</label>
<input type="number" id="split-n-value" min="1" value="5" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<p class="mt-1 text-xs text-gray-400">Each resulting PDF will contain N pages (except possibly the last one)</p>
</div>
<div id="n-times-warning" class="hidden p-3 bg-yellow-900/30 border border-yellow-500/30 rounded-lg mb-3">
<p class="text-sm text-yellow-200"><strong>Note:</strong> <span id="n-times-warning-text"></span></p>
</div>
</div>
<div id="zip-option-wrapper" class="hidden mt-4"> <div id="zip-option-wrapper" class="hidden mt-4">
<label class="flex items-center gap-2 text-sm font-medium text-gray-300"> <label class="flex items-center gap-2 text-sm font-medium text-gray-300">
<input type="checkbox" id="download-as-zip" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500"> <input type="checkbox" id="download-as-zip" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
@@ -534,9 +569,17 @@ export const toolTemplates = {
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality JPG image.</p> <p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality JPG image.</p>
${createFileInputHTML()} ${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="jpg-preview" class="hidden text-center mt-6"> <div id="jpg-preview" class="hidden mt-6">
<p class="mb-4 text-white">Click "Download All as ZIP" to get images for all pages.</p> <div class="mb-4">
<button id="process-btn" class="btn-gradient w-full mt-6">Download All as ZIP</button> <label for="jpg-quality" class="block mb-2 text-sm font-medium text-gray-300">Image Quality</label>
<div class="flex items-center gap-4">
<input type="range" id="jpg-quality" min="0.1" max="1.0" step="0.1" value="0.9" class="flex-1">
<span id="jpg-quality-value" class="text-white font-medium w-16 text-right">90%</span>
</div>
<p class="mt-1 text-xs text-gray-400">Higher quality = larger file size</p>
</div>
<p class="mb-4 text-white text-center">Click "Download All as ZIP" to get images for all pages.</p>
<button id="process-btn" class="btn-gradient w-full">Download All as ZIP</button>
</div> </div>
`, `,
'jpg-to-pdf': () => ` 'jpg-to-pdf': () => `
@@ -544,6 +587,17 @@ export const toolTemplates = {
<p class="mb-6 text-gray-400">Convert one or more JPG images into a single PDF file.</p> <p class="mb-6 text-gray-400">Convert one or more JPG images into a single PDF file.</p>
${createFileInputHTML({ multiple: true, accept: 'image/jpeg', showControls: true })} ${createFileInputHTML({ multiple: true, accept: 'image/jpeg', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="jpg-to-pdf-options" class="hidden mt-6">
<div class="mb-4">
<label for="jpg-pdf-quality" class="block mb-2 text-sm font-medium text-gray-300">PDF Quality</label>
<select id="jpg-pdf-quality" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="high">High Quality (Larger file)</option>
<option value="medium" selected>Medium Quality (Balanced)</option>
<option value="low">Low Quality (Smaller file)</option>
</select>
<p class="mt-1 text-xs text-gray-400">Controls image compression when embedding into PDF</p>
</div>
</div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button> <button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`, `,
'scan-to-pdf': () => ` 'scan-to-pdf': () => `
@@ -581,8 +635,8 @@ export const toolTemplates = {
`, `,
compress: () => ` compress: () => `
<h2 class="text-2xl font-bold text-white mb-4">Compress PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">Compress PDF</h2>
<p class="mb-6 text-gray-400">Reduce file size by choosing the compression method that best suits your document.</p> <p class="mb-6 text-gray-400">Reduce file size by choosing the compression method that best suits your document. Supports multiple PDFs.</p>
${createFileInputHTML()} ${createFileInputHTML({ multiple: true, showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="compress-options" class="hidden mt-6 space-y-6"> <div id="compress-options" class="hidden mt-6 space-y-6">
<div> <div>
@@ -706,8 +760,16 @@ export const toolTemplates = {
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality PNG image.</p> <p class="mb-6 text-gray-400">Convert each page of a PDF file into a high-quality PNG image.</p>
${createFileInputHTML()} ${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="png-preview" class="hidden text-center mt-6"> <div id="png-preview" class="hidden mt-6">
<p class="mb-4 text-white">Your file is ready. Click the button to download a ZIP file containing all PNG images.</p> <div class="mb-4">
<label for="png-quality" class="block mb-2 text-sm font-medium text-gray-300">Image Quality (Scale)</label>
<div class="flex items-center gap-4">
<input type="range" id="png-quality" min="1.0" max="4.0" step="0.5" value="2.0" class="flex-1">
<span id="png-quality-value" class="text-white font-medium w-16 text-right">2.0x</span>
</div>
<p class="mt-1 text-xs text-gray-400">Higher scale = better quality but larger file size</p>
</div>
<p class="mb-4 text-white text-center">Your file is ready. Click the button to download a ZIP file containing all PNG images.</p>
<button id="process-btn" class="btn-gradient w-full">Download All as ZIP</button> <button id="process-btn" class="btn-gradient w-full">Download All as ZIP</button>
</div> </div>
`, `,
@@ -716,6 +778,17 @@ export const toolTemplates = {
<p class="mb-6 text-gray-400">Convert one or more PNG images into a single PDF file.</p> <p class="mb-6 text-gray-400">Convert one or more PNG images into a single PDF file.</p>
${createFileInputHTML({ multiple: true, accept: 'image/png', showControls: true })} ${createFileInputHTML({ multiple: true, accept: 'image/png', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="png-to-pdf-options" class="hidden mt-6">
<div class="mb-4">
<label for="png-pdf-quality" class="block mb-2 text-sm font-medium text-gray-300">PDF Quality</label>
<select id="png-pdf-quality" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="high">High Quality (Larger file)</option>
<option value="medium" selected>Medium Quality (Balanced)</option>
<option value="low">Low Quality (Smaller file)</option>
</select>
<p class="mt-1 text-xs text-gray-400">Controls image compression when embedding into PDF</p>
</div>
</div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button> <button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`, `,
'pdf-to-webp': () => ` 'pdf-to-webp': () => `
@@ -723,8 +796,16 @@ export const toolTemplates = {
<p class="mb-6 text-gray-400">Convert each page of a PDF file into a modern WebP image.</p> <p class="mb-6 text-gray-400">Convert each page of a PDF file into a modern WebP image.</p>
${createFileInputHTML()} ${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="webp-preview" class="hidden text-center mt-6"> <div id="webp-preview" class="hidden mt-6">
<p class="mb-4 text-white">Your file is ready. Click the button to download a ZIP file containing all WebP images.</p> <div class="mb-4">
<label for="webp-quality" class="block mb-2 text-sm font-medium text-gray-300">Image Quality</label>
<div class="flex items-center gap-4">
<input type="range" id="webp-quality" min="0.1" max="1.0" step="0.1" value="0.9" class="flex-1">
<span id="webp-quality-value" class="text-white font-medium w-16 text-right">90%</span>
</div>
<p class="mt-1 text-xs text-gray-400">Higher quality = larger file size</p>
</div>
<p class="mb-4 text-white text-center">Your file is ready. Click the button to download a ZIP file containing all WebP images.</p>
<button id="process-btn" class="btn-gradient w-full">Download All as ZIP</button> <button id="process-btn" class="btn-gradient w-full">Download All as ZIP</button>
</div> </div>
`, `,
@@ -909,7 +990,18 @@ export const toolTemplates = {
<h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2> <h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2>
<p class="mb-6 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p> <p class="mb-6 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p>
${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp', showControls: true })} ${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp', showControls: true })}
<ul id="image-list" class="mt-4 grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4"></ul> <ul id="image-list" class="mt-4 grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4">
</ul>
<div id="image-to-pdf-options" class="hidden mt-6">
<div class="mb-4">
<label for="image-pdf-quality" class="block mb-2 text-sm font-medium text-gray-300">PDF Image Quality</label>
<div class="flex items-center gap-4">
<input type="range" id="image-pdf-quality" min="0.3" max="1.0" step="0.1" value="0.9" class="flex-1">
<span id="image-pdf-quality-value" class="text-white font-medium w-16 text-right">90%</span>
</div>
<p class="mt-1 text-xs text-gray-400">Higher quality = larger PDF size</p>
</div>
</div>
<button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button> <button id="process-btn" class="btn-gradient w-full mt-6">Convert to PDF</button>
`, `,
@@ -994,8 +1086,24 @@ export const toolTemplates = {
`, `,
'txt-to-pdf': () => ` 'txt-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Text to PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">Text to PDF</h2>
<p class="mb-6 text-gray-400">Type or paste your text below and convert it to a PDF with custom formatting.</p> <p class="mb-6 text-gray-400">Upload one or more text files, or type/paste text below to convert to PDF with custom formatting.</p>
<div class="mb-4">
<div class="flex gap-2 p-1 rounded-lg bg-gray-900 border border-gray-700 mb-4">
<button id="txt-mode-upload-btn" class="flex-1 btn bg-indigo-600 text-white font-semibold py-2 rounded-md">Upload Files</button>
<button id="txt-mode-text-btn" class="flex-1 btn bg-gray-700 text-gray-300 font-semibold py-2 rounded-md">Type Text</button>
</div>
<div id="txt-upload-panel">
${createFileInputHTML({ multiple: true, accept: 'text/plain,.txt', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
</div>
<div id="txt-text-panel" class="hidden">
<textarea id="text-input" rows="12" class="w-full bg-gray-900 border border-gray-600 text-gray-300 rounded-lg p-2.5 font-sans" placeholder="Start typing here..."></textarea> <textarea id="text-input" rows="12" class="w-full bg-gray-900 border border-gray-600 text-gray-300 rounded-lg p-2.5 font-sans" placeholder="Start typing here..."></textarea>
</div>
</div>
<div class="mt-6 grid grid-cols-2 md:grid-cols-4 gap-4"> <div class="mt-6 grid grid-cols-2 md:grid-cols-4 gap-4">
<div> <div>
<label for="font-family" class="block mb-2 text-sm font-medium text-gray-300">Font Family</label> <label for="font-family" class="block mb-2 text-sm font-medium text-gray-300">Font Family</label>
@@ -1981,6 +2089,23 @@ export const toolTemplates = {
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button> <button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button>
</div> </div>
`, `,
'extract-attachments': () => `
<h2 class="text-2xl font-bold text-white mb-4">Extract Attachments</h2>
<p class="mb-6 text-gray-400">Extract all embedded files from one or more PDFs. All attachments will be downloaded in a ZIP archive.</p>
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Extract Attachments</button>
`,
'edit-attachments': () => `
<h2 class="text-2xl font-bold text-white mb-4">Edit Attachments</h2>
<p class="mb-6 text-gray-400">View, remove, or replace attachments in your PDF.</p>
${createFileInputHTML({ accept: 'application/pdf' })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="edit-attachments-options" class="hidden mt-6">
<div id="attachments-list" class="space-y-3 mb-4"></div>
<button id="process-btn" class="btn-gradient w-full mt-6">Save Changes & Download</button>
</div>
`,
'sanitize-pdf': () => ` 'sanitize-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Sanitize PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">Sanitize PDF</h2>
+42
View File
@@ -0,0 +1,42 @@
let cpdfLoaded = false;
let cpdfLoadPromise: Promise<void> | null = null;
//TODO: @ALAM,is it better to use a worker to load the cpdf library?
// or just use the browser version?
export async function ensureCpdfLoaded(): Promise<void> {
if (cpdfLoaded) return;
if (cpdfLoadPromise) {
return cpdfLoadPromise;
}
cpdfLoadPromise = new Promise((resolve, reject) => {
if (typeof (window as any).coherentpdf !== 'undefined') {
cpdfLoaded = true;
resolve();
return;
}
const script = document.createElement('script');
script.src = '/coherentpdf.browser.min.js';
script.onload = () => {
cpdfLoaded = true;
resolve();
};
script.onerror = () => {
reject(new Error('Failed to load CoherentPDF library'));
};
document.head.appendChild(script);
});
return cpdfLoadPromise;
}
/**
* Gets the cpdf instance, ensuring it's loaded first
*/
export async function getCpdf(): Promise<any> {
await ensureCpdfLoaded();
return (window as any).coherentpdf;
}
+7
View File
@@ -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();
};
+147 -398
View File
@@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -8,19 +9,13 @@
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
<link href="../../src/css/bookmark.css" rel="stylesheet" /> <link href="../../src/css/bookmark.css" rel="stylesheet" />
</head> </head>
<body class="antialiased bg-gray-900"> <body class="antialiased bg-gray-900">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
class="flex-shrink-0 flex items-center cursor-pointer" <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
id="home-logo"
>
<img
src="../../public/images/favicon.svg"
alt="Bento PDF Logo"
class="h-8 w-8"
/>
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
@@ -36,47 +31,19 @@
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center"> <div class="md:hidden flex items-center">
<button <button id="mobile-menu-button" type="button"
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu" aria-controls="mobile-menu" aria-expanded="false">
aria-expanded="false"
>
<span class="sr-only">Open main menu</span> <span class="sr-only">Open main menu</span>
<!-- Hamburger Icon --> <!-- Hamburger Icon -->
<svg <svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
id="menu-icon" stroke="currentColor" aria-hidden="true">
class="block h-6 w-6" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg> </svg>
<!-- Close Icon --> <!-- Close Icon -->
<svg <svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
id="close-icon" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
class="hidden h-6 w-6" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -84,31 +51,19 @@
</div> </div>
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div <div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a> <a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="index.html#tools-header" class="mobile-nav-link" <a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
>All Tools</a
>
</div> </div>
</div> </div>
</nav> </nav>
<div <div id="uploader" class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
id="uploader" <div class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-md w-full text-gray-200 border border-gray-700">
class="min-h-screen flex items-center justify-center p-4 bg-gray-900" <button id="back-to-tools"
> class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<div
class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-md w-full text-gray-200 border border-gray-700"
>
<button
id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
>
<i data-lucide="arrow-left" class="cursor-pointer"></i> <i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer"> Back to Tools </span> <span class="cursor-pointer"> Back to Tools </span>
</button> </button>
@@ -117,15 +72,10 @@
</p> </p>
<!-- Drop Zone for Main PDF Upload --> <!-- Drop Zone for Main PDF Upload -->
<div <div id="drop-zone"
id="drop-zone" class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300">
class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300"
>
<div class="flex flex-col items-center justify-center pt-5 pb-6"> <div class="flex flex-col items-center justify-center pt-5 pb-6">
<i <i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
data-lucide="upload-cloud"
class="w-10 h-10 mb-3 text-gray-400"
></i>
<p class="mb-2 text-sm text-gray-300"> <p class="mb-2 text-sm text-gray-300">
<span class="font-semibold">Click to select a file</span> or drag <span class="font-semibold">Click to select a file</span> or drag
and drop and drop
@@ -135,12 +85,8 @@
Your files never leave your device. Your files never leave your device.
</p> </p>
</div> </div>
<input <input type="file" id="file-input" accept=".pdf"
type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
id="file-input"
accept=".pdf"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
/>
</div> </div>
<!-- File Display Area --> <!-- File Display Area -->
@@ -149,11 +95,7 @@
<!-- Auto Extract Checkbox --> <!-- Auto Extract Checkbox -->
<div class="mt-4"> <div class="mt-4">
<label class="flex items-center gap-2 text-sm text-gray-300"> <label class="flex items-center gap-2 text-sm text-gray-300">
<input <input type="checkbox" id="auto-extract-checkbox" class="w-4 h-4 accent-blue-500" />
type="checkbox"
id="auto-extract-checkbox"
class="w-4 h-4 accent-blue-500"
/>
<span>Auto-extract existing bookmarks</span> <span>Auto-extract existing bookmarks</span>
</label> </label>
</div> </div>
@@ -166,12 +108,9 @@
<!-- CSV Upload --> <!-- CSV Upload -->
<label class="block mb-4"> <label class="block mb-4">
<span class="text-xs text-gray-500" <span class="text-xs text-gray-500">CSV format: title,page,level</span>
>CSV format: title,page,level</span
>
<div <div
class="relative flex flex-col items-center justify-center w-full h-32 mt-2 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-800 hover:bg-gray-700 transition-colors duration-300" class="relative flex flex-col items-center justify-center w-full h-32 mt-2 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-800 hover:bg-gray-700 transition-colors duration-300">
>
<div class="flex flex-col items-center justify-center pt-4 pb-4"> <div class="flex flex-col items-center justify-center pt-4 pb-4">
<i data-lucide="sheet" class="w-8 h-8 mb-2 text-gray-400"></i> <i data-lucide="sheet" class="w-8 h-8 mb-2 text-gray-400"></i>
<p class="text-xs text-gray-400"> <p class="text-xs text-gray-400">
@@ -179,12 +118,8 @@
here here
</p> </p>
</div> </div>
<input <input type="file" id="csv-input" accept=".csv"
type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
id="csv-input"
accept=".csv"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
/>
</div> </div>
</label> </label>
@@ -192,8 +127,7 @@
<label class="block"> <label class="block">
<span class="text-xs text-gray-500">JSON format</span> <span class="text-xs text-gray-500">JSON format</span>
<div <div
class="relative flex flex-col items-center justify-center w-full h-32 mt-2 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-800 hover:bg-gray-700 transition-colors duration-300" class="relative flex flex-col items-center justify-center w-full h-32 mt-2 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-800 hover:bg-gray-700 transition-colors duration-300">
>
<div class="flex flex-col items-center justify-center pt-4 pb-4"> <div class="flex flex-col items-center justify-center pt-4 pb-4">
<i data-lucide="braces" class="w-8 h-8 mb-2 text-gray-400"></i> <i data-lucide="braces" class="w-8 h-8 mb-2 text-gray-400"></i>
<p class="text-xs text-gray-400"> <p class="text-xs text-gray-400">
@@ -201,12 +135,8 @@
drag here drag here
</p> </p>
</div> </div>
<input <input type="file" id="json-input" accept=".json"
type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
id="json-input"
accept=".json"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
/>
</div> </div>
</label> </label>
</div> </div>
@@ -214,41 +144,36 @@
</div> </div>
<div id="app" class="hidden bg-gray-900 min-h-screen"> <div id="app" class="hidden bg-gray-900 min-h-screen">
<header <header class="bg-gray-800 border-b border-gray-700 shadow-sm sticky top-0 z-50">
class="bg-gray-800 border-b border-gray-700 shadow-sm sticky top-0 z-50"
>
<div class="max-w-7xl mx-auto px-4 py-3"> <div class="max-w-7xl mx-auto px-4 py-3">
<div class="flex items-center justify-between flex-wrap gap-2"> <div class="flex items-center justify-between flex-wrap gap-2">
<h1 class="text-xl font-bold text-white" id="filename-display"> <h1 class="text-xl font-bold text-white" id="filename-display">
PDF Bookmark Editor PDF Bookmark Editor
</h1> </h1>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
<button <button id="undo-btn"
id="undo-btn"
class="px-3 py-2 text-sm bg-gray-700 hover:bg-gray-600 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1" class="px-3 py-2 text-sm bg-gray-700 hover:bg-gray-600 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
disabled disabled>
>
<i data-lucide="undo-2" class="w-4 h-4"></i> Undo <i data-lucide="undo-2" class="w-4 h-4"></i> Undo
</button> </button>
<button <button id="redo-btn"
id="redo-btn"
class="px-3 py-2 text-sm bg-gray-700 hover:bg-gray-600 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1" class="px-3 py-2 text-sm bg-gray-700 hover:bg-gray-600 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
disabled disabled>
>
<i data-lucide="redo-2" class="w-4 h-4"></i> Redo <i data-lucide="redo-2" class="w-4 h-4"></i> Redo
</button> </button>
<button <button id="delete-all-btn"
id="delete-all-btn" class="px-3 py-2 text-sm bg-red-600 hover:bg-red-700 text-white rounded flex items-center gap-1">
class="px-3 py-2 text-sm bg-red-600 hover:bg-red-700 text-white rounded flex items-center gap-1"
>
<i data-lucide="trash-2" class="w-4 h-4"></i> Delete All <i data-lucide="trash-2" class="w-4 h-4"></i> Delete All
</button> </button>
<button <button id="reset-btn"
id="reset-btn" class="px-3 py-2 text-sm bg-orange-600 hover:bg-orange-700 text-white rounded flex items-center gap-1">
class="px-3 py-2 text-sm bg-orange-600 hover:bg-orange-700 text-white rounded flex items-center gap-1"
>
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Reset <i data-lucide="rotate-ccw" class="w-4 h-4"></i> Reset
</button> </button>
<button id="back-btn"
class="px-3 py-2 text-sm bg-gray-700 hover:bg-gray-600 text-white rounded flex items-center gap-1">
<i data-lucide="arrow-left" class="w-4 h-4"></i> Back to Tools
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -256,16 +181,10 @@
<div class="lg:hidden bg-gray-800 border-b border-gray-700"> <div class="lg:hidden bg-gray-800 border-b border-gray-700">
<div class="flex"> <div class="flex">
<button <button id="show-viewer-btn" class="flex-1 py-3 text-sm font-medium bg-indigo-600 text-white">
id="show-viewer-btn"
class="flex-1 py-3 text-sm font-medium bg-indigo-600 text-white"
>
PDF Viewer PDF Viewer
</button> </button>
<button <button id="show-bookmarks-btn" class="flex-1 py-3 text-sm font-medium text-gray-300 hover:text-white">
id="show-bookmarks-btn"
class="flex-1 py-3 text-sm font-medium text-gray-300 hover:text-white"
>
Bookmarks Bookmarks
</button> </button>
</div> </div>
@@ -275,194 +194,113 @@
<div class="flex flex-col lg:flex-row gap-4"> <div class="flex flex-col lg:flex-row gap-4">
<div id="viewer-section" class="w-full lg:w-[50%] flex-shrink-0"> <div id="viewer-section" class="w-full lg:w-[50%] flex-shrink-0">
<div class="bg-white rounded-lg shadow-lg p-4"> <div class="bg-white rounded-lg shadow-lg p-4">
<div <div class="flex flex-wrap items-center justify-between gap-2 mb-4">
class="flex flex-wrap items-center justify-between gap-2 mb-4"
>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button id="prev-page" class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm">
id="prev-page" <i data-lucide="chevron-left" class="w-5 h-5 text-blue-600"></i>
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm"
>
←
</button> </button>
<span <span id="page-indicator" class="text-sm font-medium text-gray-700 whitespace-nowrap">Page 1 / 1</span>
id="page-indicator" <button id="next-page" class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm">
class="text-sm font-medium text-gray-700 whitespace-nowrap" <i data-lucide="chevron-right" class="w-5 h-5 text-blue-600"></i>
>Page 1 / 1</span
>
<button
id="next-page"
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm"
>
→
</button> </button>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm text-gray-600">Go to:</span> <span class="text-sm text-gray-600">Go to:</span>
<input <input type="number" id="goto-page" min="1"
type="number" class="w-16 px-2 py-1 border border-gray-300 rounded text-sm" />
id="goto-page" <button id="goto-btn" class="px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm">
min="1"
class="w-16 px-2 py-1 border border-gray-300 rounded text-sm"
/>
<button
id="goto-btn"
class="px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm"
>
Go Go
</button> </button>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button <button id="zoom-out-btn" class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm"
id="zoom-out-btn" title="Zoom Out">
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm" <i data-lucide="minus" class="w-5 h-5 text-blue-600"></i>
title="Zoom Out"
>
−
</button> </button>
<span <span id="zoom-indicator"
id="zoom-indicator" class="text-sm font-medium text-gray-700 whitespace-nowrap min-w-[60px] text-center">100%</span>
class="text-sm font-medium text-gray-700 whitespace-nowrap min-w-[60px] text-center" <button id="zoom-in-btn" class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm"
>100%</span title="Zoom In">
> <i data-lucide="plus" class="w-5 h-5 text-blue-600"></i>
<button
id="zoom-in-btn"
class="px-3 py-2 bg-gray-100 hover:bg-gray-200 rounded text-sm"
title="Zoom In"
>
+
</button> </button>
<button <button id="zoom-fit-btn" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded text-sm"
id="zoom-fit-btn" title="Fit to Width">
class="px-3 py-1 bg-gray-100 hover:bg-gray-200 rounded text-sm"
title="Fit to Width"
>
Fit Fit
</button> </button>
</div> </div>
</div> </div>
<div <div id="picking-mode-banner" class="hidden mb-4 p-3 bg-blue-100 border-2 border-blue-400 rounded-lg">
id="picking-mode-banner"
class="hidden mb-4 p-3 bg-blue-100 border-2 border-blue-400 rounded-lg"
>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<i <i data-lucide="crosshair" class="w-5 h-5 text-blue-600"></i>
data-lucide="crosshair" <span class="text-sm font-medium text-blue-800">Click on the PDF to set bookmark destination</span>
class="w-5 h-5 text-blue-600"
></i>
<span class="text-sm font-medium text-blue-800"
>Click on the PDF to set bookmark destination</span
>
</div> </div>
<button <button id="cancel-picking-btn"
id="cancel-picking-btn" class="px-3 py-1 bg-red-500 hover:bg-red-600 text-white rounded text-xs">
class="px-3 py-1 bg-red-500 hover:bg-red-600 text-white rounded text-xs"
>
Cancel Cancel
</button> </button>
</div> </div>
</div> </div>
<div <div class="overflow-auto border border-gray-200 rounded" id="canvas-container">
class="overflow-auto border border-gray-200 rounded"
id="canvas-container"
>
<div class="pdf-canvas-wrapper" id="pdf-canvas-wrapper"> <div class="pdf-canvas-wrapper" id="pdf-canvas-wrapper">
<canvas <canvas id="pdf-canvas" class="max-w-full h-auto mx-auto"></canvas>
id="pdf-canvas"
class="max-w-full h-auto mx-auto"
></canvas>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div <div id="bookmarks-section" class="hidden lg:block w-full lg:w-[50%] flex-shrink-0">
id="bookmarks-section"
class="hidden lg:block w-full lg:w-[50%] flex-shrink-0"
>
<div class="bg-white rounded-lg shadow-lg p-4"> <div class="bg-white rounded-lg shadow-lg p-4">
<h3 class="text-lg font-bold text-gray-800 mb-4">Bookmarks</h3> <h3 class="text-lg font-bold text-gray-800 mb-4">Bookmarks</h3>
<div class="mb-4"> <div class="mb-4">
<div class="relative"> <div class="relative">
<i <i data-lucide="search"
data-lucide="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400"></i>
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" <input type="text" id="search-bookmarks" placeholder="Search bookmarks..."
></i> class="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg text-sm" />
<input
type="text"
id="search-bookmarks"
placeholder="Search bookmarks..."
class="w-full pl-10 pr-3 py-2 border border-gray-300 rounded-lg text-sm"
/>
</div> </div>
</div> </div>
<div class="mb-4 p-3 bg-gray-50 rounded-lg"> <div class="mb-4 p-3 bg-gray-50 rounded-lg">
<input <input type="text" id="bookmark-title" placeholder="Bookmark title..."
type="text" class="w-full px-3 py-2 border border-gray-300 rounded-lg mb-2 text-sm" />
id="bookmark-title" <button id="add-top-level-btn"
placeholder="Bookmark title..." class="w-full px-3 py-2 bg-gradient-to-b from-blue-500 to-blue-600 hover:shadow-xl text-white rounded text-sm font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
class="w-full px-3 py-2 border border-gray-300 rounded-lg mb-2 text-sm"
/>
<button
id="add-top-level-btn"
class="w-full px-3 py-2 bg-gradient-to-b from-blue-500 to-blue-600 hover:shadow-xl text-white rounded text-sm font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200"
>
<i data-lucide="plus" class="w-4 h-4"></i> <i data-lucide="plus" class="w-4 h-4"></i>
<span <span>Add to Page <span id="current-page-display">1</span></span>
>Add to Page <span id="current-page-display">1</span></span
>
</button> </button>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label <label class="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
class="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2" <input type="checkbox" id="batch-mode-checkbox" class="w-4 h-4" />
> <span>Batch Mode (<span id="selected-count">0</span>
<input selected)</span>
type="checkbox"
id="batch-mode-checkbox"
class="w-4 h-4"
/>
<span
>Batch Mode (<span id="selected-count">0</span>
selected)</span
>
</label> </label>
<div <div id="batch-operations" class="hidden p-3 bg-blue-50 rounded-lg border border-blue-200">
id="batch-operations"
class="hidden p-3 bg-blue-50 rounded-lg border border-blue-200"
>
<div class="text-xs font-semibold text-gray-700 mb-2"> <div class="text-xs font-semibold text-gray-700 mb-2">
Batch Operations Batch Operations
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex gap-2"> <div class="flex gap-2">
<button <button id="select-all-btn"
id="select-all-btn" class="flex-1 px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-xs flex items-center justify-center gap-1">
class="flex-1 px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-xs flex items-center justify-center gap-1"
>
<i data-lucide="check-square" class="w-3 h-3"></i> <i data-lucide="check-square" class="w-3 h-3"></i>
Select All Select All
</button> </button>
<button <button id="deselect-all-btn"
id="deselect-all-btn" class="flex-1 px-3 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded text-xs flex items-center justify-center gap-1">
class="flex-1 px-3 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded text-xs flex items-center justify-center gap-1"
>
<i data-lucide="square" class="w-3 h-3"></i> Deselect <i data-lucide="square" class="w-3 h-3"></i> Deselect
All All
</button> </button>
</div> </div>
<select <select id="batch-color-select" class="text-xs p-2 border rounded">
id="batch-color-select"
class="text-xs p-2 border rounded"
>
<option value="">Set Color...</option> <option value="">Set Color...</option>
<option value="null">None</option> <option value="null">None</option>
<option value="red">Red</option> <option value="red">Red</option>
@@ -472,10 +310,7 @@
<option value="purple">Purple</option> <option value="purple">Purple</option>
</select> </select>
<select <select id="batch-style-select" class="text-xs p-2 border rounded">
id="batch-style-select"
class="text-xs p-2 border rounded"
>
<option value="">Set Style...</option> <option value="">Set Style...</option>
<option value="null">Normal</option> <option value="null">Normal</option>
<option value="bold">Bold</option> <option value="bold">Bold</option>
@@ -483,10 +318,8 @@
<option value="bold-italic">Bold & Italic</option> <option value="bold-italic">Bold & Italic</option>
</select> </select>
<button <button id="batch-delete-btn"
id="batch-delete-btn" class="px-3 py-2 bg-red-500 hover:bg-red-600 text-white rounded text-xs flex items-center justify-center gap-1">
class="px-3 py-2 bg-red-500 hover:bg-red-600 text-white rounded text-xs flex items-center justify-center gap-1"
>
<i data-lucide="trash-2" class="w-3 h-3"></i> Delete <i data-lucide="trash-2" class="w-3 h-3"></i> Delete
Selected Selected
</button> </button>
@@ -495,47 +328,32 @@
</div> </div>
<div class="flex gap-2 mb-4"> <div class="flex gap-2 mb-4">
<button <button id="expand-all-btn"
id="expand-all-btn" class="flex-1 px-3 py-2 bg-gradient-to-b from-gray-100 to-gray-200 hover:shadow-md text-gray-700 rounded text-sm font-medium flex items-center justify-center gap-1 focus:ring-2 focus:ring-gray-300 transition duration-200">
class="flex-1 px-3 py-2 bg-gradient-to-b from-gray-100 to-gray-200 hover:shadow-md text-gray-700 rounded text-sm font-medium flex items-center justify-center gap-1 focus:ring-2 focus:ring-gray-300 transition duration-200"
>
<i data-lucide="chevrons-down" class="w-4 h-4"></i> Expand All <i data-lucide="chevrons-down" class="w-4 h-4"></i> Expand All
</button> </button>
<button <button id="collapse-all-btn"
id="collapse-all-btn" class="flex-1 px-3 py-2 bg-gradient-to-b from-gray-100 to-gray-200 hover:shadow-md text-gray-700 rounded text-sm font-medium flex items-center justify-center gap-1 focus:ring-2 focus:ring-gray-300 transition duration-200">
class="flex-1 px-3 py-2 bg-gradient-to-b from-gray-100 to-gray-200 hover:shadow-md text-gray-700 rounded text-sm font-medium flex items-center justify-center gap-1 focus:ring-2 focus:ring-gray-300 transition duration-200"
>
<i data-lucide="chevrons-up" class="w-4 h-4"></i> Collapse All <i data-lucide="chevrons-up" class="w-4 h-4"></i> Collapse All
</button> </button>
</div> </div>
<div class="grid grid-cols-2 gap-2 mb-4"> <div class="grid grid-cols-2 gap-2 mb-4">
<div class="relative"> <div class="relative">
<button <button id="import-dropdown-btn"
id="import-dropdown-btn" class="w-full px-3 py-2 bg-gradient-to-b from-gray-100 to-gray-200 hover:shadow-md text-gray-700 rounded text-sm font-medium flex items-center justify-center gap-1 focus:ring-2 focus:ring-gray-300 transition duration-200">
class="w-full px-3 py-2 bg-gradient-to-b from-gray-100 to-gray-200 hover:shadow-md text-gray-700 rounded text-sm font-medium flex items-center justify-center gap-1 focus:ring-2 focus:ring-gray-300 transition duration-200"
>
<i data-lucide="upload" class="w-4 h-4"></i> Import <i data-lucide="upload" class="w-4 h-4"></i> Import
<i data-lucide="chevron-down" class="w-3 h-3"></i> <i data-lucide="chevron-down" class="w-3 h-3"></i>
</button> </button>
<div <div id="import-dropdown"
id="import-dropdown" class="hidden absolute top-full left-0 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-lg z-10">
class="hidden absolute top-full left-0 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-lg z-10" <button id="import-csv-btn"
> class="w-full px-3 py-2 text-sm text-left flex items-center gap-2 rounded-t-lg bg-gradient-to-b from-gray-50 to-gray-100 hover:shadow-sm hover:from-gray-100 hover:to-gray-200 transition duration-150">
<button <i data-lucide="file-text" class="w-4 h-4 text-gray-600"></i>
id="import-csv-btn"
class="w-full px-3 py-2 text-sm text-left flex items-center gap-2 rounded-t-lg bg-gradient-to-b from-gray-50 to-gray-100 hover:shadow-sm hover:from-gray-100 hover:to-gray-200 transition duration-150"
>
<i
data-lucide="file-text"
class="w-4 h-4 text-gray-600"
></i>
Import CSV Import CSV
</button> </button>
<button <button id="import-json-btn"
id="import-json-btn" class="w-full px-3 py-2 text-sm text-left flex items-center gap-2 rounded-b-lg bg-gradient-to-b from-gray-50 to-gray-100 hover:shadow-sm hover:from-gray-100 hover:to-gray-200 transition duration-150">
class="w-full px-3 py-2 text-sm text-left flex items-center gap-2 rounded-b-lg bg-gradient-to-b from-gray-50 to-gray-100 hover:shadow-sm hover:from-gray-100 hover:to-gray-200 transition duration-150"
>
<i data-lucide="braces" class="w-4 h-4 text-gray-600"></i> <i data-lucide="braces" class="w-4 h-4 text-gray-600"></i>
Import JSON Import JSON
</button> </button>
@@ -543,56 +361,39 @@
</div> </div>
<div class="relative"> <div class="relative">
<button <button id="export-dropdown-btn"
id="export-dropdown-btn" class="w-full px-3 py-2 bg-gradient-to-b from-gray-100 to-gray-200 hover:shadow-md text-gray-700 rounded text-sm font-medium flex items-center justify-center gap-1 focus:ring-2 focus:ring-gray-300 transition duration-200">
class="w-full px-3 py-2 bg-gradient-to-b from-gray-100 to-gray-200 hover:shadow-md text-gray-700 rounded text-sm font-medium flex items-center justify-center gap-1 focus:ring-2 focus:ring-gray-300 transition duration-200"
>
<i data-lucide="download" class="w-4 h-4"></i> Export <i data-lucide="download" class="w-4 h-4"></i> Export
<i data-lucide="chevron-down" class="w-3 h-3"></i> <i data-lucide="chevron-down" class="w-3 h-3"></i>
</button> </button>
<div <div id="export-dropdown"
id="export-dropdown" class="hidden absolute top-full left-0 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-lg z-10">
class="hidden absolute top-full left-0 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-lg z-10" <button id="export-csv-btn"
> class="w-full px-3 py-2 text-sm text-left hover:bg-gray-50 flex items-center gap-2 rounded-t-lg">
<button
id="export-csv-btn"
class="w-full px-3 py-2 text-sm text-left hover:bg-gray-50 flex items-center gap-2 rounded-t-lg"
>
<i data-lucide="file-text" class="w-4 h-4"></i> Export CSV <i data-lucide="file-text" class="w-4 h-4"></i> Export CSV
</button> </button>
<button <button id="export-json-btn"
id="export-json-btn" class="w-full px-3 py-2 text-sm text-left hover:bg-gray-50 flex items-center gap-2 rounded-b-lg">
class="w-full px-3 py-2 text-sm text-left hover:bg-gray-50 flex items-center gap-2 rounded-b-lg"
>
<i data-lucide="braces" class="w-4 h-4"></i> Export JSON <i data-lucide="braces" class="w-4 h-4"></i> Export JSON
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<div <div class="mb-4 max-h-96 overflow-y-auto border border-gray-200 rounded-lg p-2">
class="mb-4 max-h-96 overflow-y-auto border border-gray-200 rounded-lg p-2"
>
<ul id="bookmark-tree-list" class="space-y-2"></ul> <ul id="bookmark-tree-list" class="space-y-2"></ul>
<div <div id="no-bookmarks" class="text-center py-8 text-gray-400 text-sm">
id="no-bookmarks"
class="text-center py-8 text-gray-400 text-sm"
>
No bookmarks yet. Add one above! No bookmarks yet. Add one above!
</div> </div>
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<button <button id="download-btn"
id="download-btn" class="w-full px-4 py-2 bg-gradient-to-b from-blue-600 to-blue-700 hover:shadow-xl text-white rounded font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
class="w-full px-4 py-2 bg-gradient-to-b from-blue-600 to-blue-700 hover:shadow-xl text-white rounded font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200"
>
Save PDF with Bookmarks Save PDF with Bookmarks
</button> </button>
<button <button id="extract-existing-btn"
id="extract-existing-btn" class="w-full px-4 py-2 bg-gradient-to-b from-green-500 to-green-600 hover:shadow-xl text-white rounded font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-green-400 transition duration-200">
class="w-full px-4 py-2 bg-gradient-to-b from-green-500 to-green-600 hover:shadow-xl text-white rounded font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-green-400 transition duration-200"
>
Extract Existing Bookmarks Extract Existing Bookmarks
</button> </button>
</div> </div>
@@ -609,16 +410,10 @@
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]"> <footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div <div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
>
<div class="mb-8 md:mb-0"> <div class="mb-8 md:mb-0">
<div class="flex items-center justify-center md:justify-start mb-4"> <div class="flex items-center justify-center md:justify-start mb-4">
<img <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
src="../../public/images/favicon.svg"
alt="Bento PDF Logo"
class="h-10 w-10 mr-3"
/>
<span class="text-xl font-bold text-white">BentoPDF</span> <span class="text-xl font-bold text-white">BentoPDF</span>
</div> </div>
<p class="text-gray-400 text-sm"> <p class="text-gray-400 text-sm">
@@ -630,17 +425,13 @@
<h3 class="font-bold text-white mb-4">Company</h3> <h3 class="font-bold text-white mb-4">Company</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="./about.html" class="hover:text-indigo-400" <a href="./about.html" class="hover:text-indigo-400">About Us</a>
>About Us</a
>
</li> </li>
<li> <li>
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a> <a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
</li> </li>
<li> <li>
<a href="./contact.html" class="hover:text-indigo-400" <a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
>Contact Us</a
>
</li> </li>
</ul> </ul>
</div> </div>
@@ -649,14 +440,10 @@
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="./terms.html" class="hover:text-indigo-400" <a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
>Terms and Conditions</a
>
</li> </li>
<li> <li>
<a href="./privacy.html" class="hover:text-indigo-400" <a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
>Privacy Policy</a
>
</li> </li>
</ul> </ul>
</div> </div>
@@ -664,72 +451,33 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Follow Us</h3> <h3 class="font-bold text-white mb-4">Follow Us</h3>
<div class="flex justify-center md:justify-start space-x-4"> <div class="flex justify-center md:justify-start space-x-4">
<a <a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
href="https://github.com/alam00000/bentopdf" class="text-gray-400 hover:text-indigo-400" title="GitHub">
target="_blank" <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
rel="noopener noreferrer" <path fill-rule="evenodd"
class="text-gray-400 hover:text-indigo-400"
title="GitHub"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" clip-rule="evenodd" />
/>
</svg> </svg>
</a> </a>
<a <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
href="https://discord.gg/q42xWQmJ" class="text-gray-400 hover:text-indigo-400" title="Discord">
target="_blank" <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400"
title="Discord"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path <path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
/>
</svg> </svg>
</a> </a>
<a <a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
href="https://www.instagram.com/thebentopdf/" title="Instagram">
class="text-gray-400 hover:text-indigo-400"
title="Instagram"
>
<i data-lucide="instagram"></i> <i data-lucide="instagram"></i>
</a> </a>
<a <a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
href="https://www.linkedin.com/company/bentopdf/" title="LinkedIn">
class="text-gray-400 hover:text-indigo-400"
title="LinkedIn"
>
<i data-lucide="linkedin"></i> <i data-lucide="linkedin"></i>
</a> </a>
<a <a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
href="https://x.com/BentoPDF" <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
class="text-gray-400 hover:text-indigo-400"
title="X (Twitter)"
>
<svg
class="w-6 h-6"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path <path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
/>
</svg> </svg>
</a> </a>
</div> </div>
@@ -741,4 +489,5 @@
<script type="module" src="../js/logic/bookmark-pdf.ts"></script> <script type="module" src="../js/logic/bookmark-pdf.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+18 -19
View File
@@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -7,6 +8,7 @@
<link rel="icon" type="image/png" href="../../images/favicon.svg" /> <link rel="icon" type="image/png" href="../../images/favicon.svg" />
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
</head> </head>
<body class="antialiased bg-gray-900"> <body class="antialiased bg-gray-900">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
@@ -14,16 +16,16 @@
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="../../index.html">BentoPDF</a> <a href="/">BentoPDF</a>
</span> </span>
</div> </div>
<!-- Desktop Navigation --> <!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8 text-white"> <div class="hidden md:flex items-center space-x-8 text-white">
<a href="../../index.html" class="nav-link">Home</a> <a href="/" class="nav-link">Home</a>
<a href="../../about.html" class="nav-link">About</a> <a href="/about.html" class="nav-link">About</a>
<a href="../../contact.html" class="nav-link">Contact</a> <a href="/contact.html" class="nav-link">Contact</a>
<a href="../../index.html#tools-header" class="nav-link">All Tools</a> <a href="/" class="nav-link">All Tools</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
@@ -48,10 +50,10 @@
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="../../index.html" class="mobile-nav-link">Home</a> <a href="/" class="mobile-nav-link">Home</a>
<a href="../../about.html" class="mobile-nav-link">About</a> <a href="/about.html" class="mobile-nav-link">About</a>
<a href="../../contact.html" class="mobile-nav-link">Contact</a> <a href="/contact.html" class="mobile-nav-link">Contact</a>
<a href="../../index.html#tools-header" class="mobile-nav-link">All Tools</a> <a href="/" class="mobile-nav-link">All Tools</a>
</div> </div>
</div> </div>
</nav> </nav>
@@ -69,12 +71,14 @@
</p> </p>
<div class="bg-yellow-900/30 border border-yellow-700 rounded-lg p-3 mb-6"> <div class="bg-yellow-900/30 border border-yellow-700 rounded-lg p-3 mb-6">
<p class="text-yellow-200 text-sm"> <p class="text-yellow-200 text-sm">
<strong>Note:</strong> Only JSON files created by the PDF-to-JSON converter tool are supported. Standard JSON files from other tools will not work. <strong>Note:</strong> Only JSON files created by the PDF-to-JSON converter tool are supported. Standard JSON
files from other tools will not work.
</p> </p>
</div> </div>
<div class="upload-section mb-6"> <div class="upload-section mb-6">
<div class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300"> <div
class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300">
<div class="flex flex-col items-center justify-center pt-5 pb-6"> <div class="flex flex-col items-center justify-center pt-5 pb-6">
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i> <i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
<p class="mb-2 text-sm text-gray-300"> <p class="mb-2 text-sm text-gray-300">
@@ -83,13 +87,8 @@
<p class="text-xs text-gray-500">Multiple JSON files</p> <p class="text-xs text-gray-500">Multiple JSON files</p>
<p class="text-xs text-gray-500">Your files never leave your device.</p> <p class="text-xs text-gray-500">Your files never leave your device.</p>
</div> </div>
<input <input type="file" id="jsonFiles" accept="application/json" multiple
type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
id="jsonFiles"
accept="application/json"
multiple
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
/>
</div> </div>
<!-- File Display Area --> <!-- File Display Area -->
@@ -109,5 +108,5 @@
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
</html>
</html>
+227
View File
@@ -0,0 +1,227 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PDF Multi Tool - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
<link href="../../src/css/styles.css" rel="stylesheet" />
<style>
/* Hide scrollbar for toolbar overflow */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</head>
<body class="antialiased bg-gray-900 h-screen overflow-hidden">
<!-- Navigation -->
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center">
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<span class="text-white font-bold text-xl ml-2">
<a href="/">BentoPDF</a>
</span>
<span class="text-gray-400 ml-3 text-sm sm:text-base">PDF Multi Tool</span>
</div>
<button id="close-tool-btn"
class="flex items-center gap-1 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="x" class="w-4 h-4 sm:w-5 sm:h-5"></i> Close
</button>
</div>
</div>
</nav>
<!-- Main Container -->
<div class="flex flex-col h-[calc(100vh-4rem)]">
<!-- Toolbar -->
<div class="bg-gray-800 border-b border-gray-700 p-2 sm:p-3 overflow-x-auto scrollbar-hide">
<div
class="flex flex-wrap items-center justify-center sm:justify-start gap-2 bg-gray-900 p-2 sm:p-4 rounded-lg w-full">
<!-- Upload -->
<button id="upload-pdfs-btn"
class="flex items-center gap-1 sm:gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="upload" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden sm:inline">Upload PDFs</span>
<span class="sm:hidden">Upload</span>
</button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<!-- Add Blank -->
<button id="add-blank-page-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="file-plus" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden sm:inline">Add Blank Page</span>
</button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<span class="text-gray-400 text-xs sm:text-sm md:inline">Edit:</span>
<!-- Undo / Redo / Reset -->
<button id="undo-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="undo-2" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Undo</span>
</button>
<button id="redo-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="redo-2" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Redo</span>
</button>
<button id="reset-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="refresh-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Reset</span>
</button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<!-- Selection -->
<span class="text-gray-400 text-xs sm:text-sm md:inline">Selection:</span>
<button id="select-all-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="check-square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Select All</span>
</button>
<button id="deselect-all-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="square" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden lg:inline">Deselect All</span>
</button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<!-- Rotate -->
<span class="text-gray-400 text-xs sm:text-sm md:inline">Rotate:</span>
<button id="bulk-rotate-left-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Left</span>
</button>
<button id="bulk-rotate-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Right</span>
</button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<span class="text-gray-400 text-xs sm:text-sm md:inline">Transform:</span>
<!-- Duplicate / Split -->
<button id="bulk-duplicate-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="copy" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Duplicate</span>
</button>
<button id="bulk-split-btn"
class="flex items-center gap-1 sm:gap-2 bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="scissors" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Split</span>
</button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<span class="text-gray-400 text-xs sm:text-sm md:inline">Clear:</span>
<!-- Delete -->
<button id="bulk-delete-btn"
class="flex items-center gap-1 sm:gap-2 bg-red-500 hover:bg-red-700 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="trash-2" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Delete</span>
</button>
<div class="border-l border-gray-600 h-5 sm:h-6 mx-1"></div>
<!-- Download -->
<span class="text-gray-400 text-xs sm:text-sm md:inline">Download:</span>
<button id="bulk-download-btn"
class="flex items-center gap-1 sm:gap-2 bg-green-600 hover:bg-green-700 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<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>
<button id="export-pdf-btn"
class="flex items-center gap-1 sm:gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 rounded text-xs sm:text-sm md:text-base">
<i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
<span class="hidden xl:inline">Export PDF</span>
</button>
</div>
</div>
<!-- Content Area -->
<div class="flex-1 overflow-auto p-3 sm:p-4">
<div id="upload-area"
class="hidden border-2 border-dashed border-gray-600 rounded-lg p-6 sm:p-12 text-center max-w-full">
<i data-lucide="upload-cloud" class="w-12 h-12 sm:w-16 sm:h-16 mx-auto text-gray-400 mb-3 sm:mb-4"></i>
<p class="text-gray-300 text-base sm:text-lg mb-1 sm:mb-2">Upload PDF Files</p>
<p class="text-gray-400 text-xs sm:text-sm mb-3 sm:mb-4">Drag and drop PDF files here, or click to select</p>
<input type="file" id="pdf-file-input" accept="application/pdf" multiple class="hidden">
<button onclick="document.getElementById('pdf-file-input').click()"
class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 sm:px-6 py-2 rounded text-sm sm:text-base">
Select Files
</button>
</div>
<div id="pages-container"
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 sm:gap-4"></div>
</div>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="hidden fixed inset-0 bg-black/80 z-50 flex items-center justify-center">
<div class="bg-gray-800 rounded-lg p-8 max-w-md w-full mx-4">
<div class="flex items-center justify-center mb-4">
<i data-lucide="loader" class="w-12 h-12 text-indigo-400 animate-spin"></i>
</div>
<p id="loading-text" class="text-white text-center text-lg mb-4">Rendering pages...</p>
<div class="w-full bg-gray-700 rounded-full h-2.5">
<div id="loading-progress" class="bg-indigo-600 h-2.5 rounded-full transition-all duration-300"
style="width: 0%"></div>
</div>
</div>
</div>
<!-- Modal -->
<div id="modal" class="hidden fixed inset-0 bg-black/80 z-50 flex items-center justify-center">
<div class="bg-gray-800 rounded-lg p-8 max-w-md w-full mx-4 border border-gray-700">
<div class="flex items-center justify-center mb-4">
<div id="modal-icon"></div>
</div>
<h3 id="modal-title" class="text-xl font-bold text-white text-center mb-2"></h3>
<p id="modal-message" class="text-gray-300 text-center mb-6"></p>
<button id="modal-close-btn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded">
Close
</button>
</div>
</div>
<!-- Hidden File Input -->
<input type="file" id="insert-pdf-input" accept="application/pdf" class="hidden">
<!-- Scripts -->
<script type="module" src="../js/logic/pdf-multi-tool.ts"></script>
</body>
</html>
+16 -18
View File
@@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -7,6 +8,7 @@
<link rel="icon" type="image/png" href="../../images/favicon.svg" /> <link rel="icon" type="image/png" href="../../images/favicon.svg" />
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
</head> </head>
<body class="antialiased bg-gray-900"> <body class="antialiased bg-gray-900">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
@@ -14,16 +16,16 @@
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="../../index.html">BentoPDF</a> <a href="/">BentoPDF</a>
</span> </span>
</div> </div>
<!-- Desktop Navigation --> <!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8 text-white"> <div class="hidden md:flex items-center space-x-8 text-white">
<a href="../../index.html" class="nav-link">Home</a> <a href="/" class="nav-link">Home</a>
<a href="../../about.html" class="nav-link">About</a> <a href="/about.html" class="nav-link">About</a>
<a href="../../contact.html" class="nav-link">Contact</a> <a href="/contact.html" class="nav-link">Contact</a>
<a href="../../index.html#tools-header" class="nav-link">All Tools</a> <a href="/" class="nav-link">All Tools</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
@@ -48,10 +50,10 @@
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="../../index.html" class="mobile-nav-link">Home</a> <a href="/" class="mobile-nav-link">Home</a>
<a href="../../about.html" class="mobile-nav-link">About</a> <a href="/about.html" class="mobile-nav-link">About</a>
<a href="../../contact.html" class="mobile-nav-link">Contact</a> <a href="/contact.html" class="mobile-nav-link">Contact</a>
<a href="../../index.html#tools-header" class="mobile-nav-link">All Tools</a> <a href="/" class="mobile-nav-link">All Tools</a>
</div> </div>
</div> </div>
</nav> </nav>
@@ -69,7 +71,8 @@
</p> </p>
<div class="upload-section mb-6"> <div class="upload-section mb-6">
<div class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300"> <div
class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300">
<div class="flex flex-col items-center justify-center pt-5 pb-6"> <div class="flex flex-col items-center justify-center pt-5 pb-6">
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i> <i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
<p class="mb-2 text-sm text-gray-300"> <p class="mb-2 text-sm text-gray-300">
@@ -78,13 +81,8 @@
<p class="text-xs text-gray-500">Multiple PDF files</p> <p class="text-xs text-gray-500">Multiple PDF files</p>
<p class="text-xs text-gray-500">Your files never leave your device.</p> <p class="text-xs text-gray-500">Your files never leave your device.</p>
</div> </div>
<input <input type="file" id="pdfFiles" accept="application/pdf" multiple
type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
id="pdfFiles"
accept="application/pdf"
multiple
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
/>
</div> </div>
<!-- File Display Area --> <!-- File Display Area -->
@@ -104,5 +102,5 @@
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
</html>
</html>
+9 -9
View File
@@ -16,16 +16,16 @@
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="../../index.html">BentoPDF</a> <a href="/">BentoPDF</a>
</span> </span>
</div> </div>
<!-- Desktop Navigation --> <!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8 text-white"> <div class="hidden md:flex items-center space-x-8 text-white">
<a href="../../index.html" class="nav-link">Home</a> <a href="/" class="nav-link">Home</a>
<a href="../../about.html" class="nav-link">About</a> <a href="/about.html" class="nav-link">About</a>
<a href="../../contact.html" class="nav-link">Contact</a> <a href="/contact.html" class="nav-link">Contact</a>
<a href="../../index.html#tools-header" class="nav-link">All Tools</a> <a href="/" class="nav-link">All Tools</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
@@ -52,10 +52,10 @@
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="../../index.html" class="mobile-nav-link">Home</a> <a href="/" class="mobile-nav-link">Home</a>
<a href="../../about.html" class="mobile-nav-link">About</a> <a href="/about.html" class="mobile-nav-link">About</a>
<a href="../../contact.html" class="mobile-nav-link">Contact</a> <a href="/contact.html" class="mobile-nav-link">Contact</a>
<a href="../../index.html#tools-header" class="mobile-nav-link">All Tools</a> <a href="/" class="mobile-nav-link">All Tools</a>
</div> </div>
</div> </div>
</nav> </nav>
+1540 -43
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -50,6 +50,7 @@ export default defineConfig(({ mode }) => ({
), ),
'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'), 'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'),
'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'), 'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'),
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
}, },
}, },
}, },