refactor: streamline HTML structure and enhance UI components
- Cleaned up the HTML structure in index.html and pdf-multi-tool.html for better readability and maintainability. - Improved the user interface of the PDF multi-tool with responsive button designs and better layout. - Added new CSS styles for button states and cursor behavior. - Updated README with corrected Docker deployment link. - Refactored JavaScript logic to utilize new helper functions for formatting star counts. - Commented out unused attachment functionalities in the logic files for future integration.
This commit is contained in:
@@ -105,7 +105,7 @@ You can run BentoPDF locally for development or personal use.
|
||||
|
||||
### 🚀 Quick Start with Docker
|
||||
|
||||
[](https://zeabur.com/templates/LWO8I0?referralCode=LokiSalmonNeko)
|
||||
[](https://zeabur.com/templates/K4AU2B)
|
||||
|
||||
You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository:
|
||||
|
||||
|
||||
552
index.html
552
index.html
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div
|
||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||
id="home-logo"
|
||||
>
|
||||
<img
|
||||
src="images/favicon.svg"
|
||||
alt="Bento PDF Logo"
|
||||
class="h-8 w-8"
|
||||
/>
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" 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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
@@ -36,47 +30,19 @@
|
||||
|
||||
<!-- Mobile Hamburger Button -->
|
||||
<div class="md:hidden flex items-center">
|
||||
<button
|
||||
id="mobile-menu-button"
|
||||
type="button"
|
||||
<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"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false"
|
||||
>
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!-- Hamburger Icon -->
|
||||
<svg
|
||||
id="menu-icon"
|
||||
class="block h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<!-- Close Icon -->
|
||||
<svg
|
||||
id="close-icon"
|
||||
class="hidden h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -84,17 +50,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||
>All Tools</a
|
||||
>
|
||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -103,63 +64,47 @@
|
||||
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
|
||||
The <span class="marker-slanted"> PDF Toolkit </span> built for
|
||||
privacy<span
|
||||
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500"
|
||||
>.</span
|
||||
>
|
||||
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
|
||||
</h1>
|
||||
<p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p>
|
||||
<div
|
||||
class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8"
|
||||
>
|
||||
<div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8">
|
||||
<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>
|
||||
No Signups
|
||||
</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>
|
||||
Unlimited Use
|
||||
</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>
|
||||
Works Offline
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<a
|
||||
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 Using - Forever Free</a
|
||||
>
|
||||
<a
|
||||
href="https://github.com/alam00000/bentopdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-indigo-400 underline transition-colors"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
<a 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
|
||||
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"
|
||||
/>
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
<span>View on GitHub</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<i data-lucide="star" class="w-3 h-3"></i>
|
||||
|
||||
<span id="github-stars">-</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
@@ -173,10 +118,7 @@
|
||||
<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="flex items-center gap-4 mb-3">
|
||||
<i
|
||||
data-lucide="user-plus"
|
||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
||||
></i>
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">
|
||||
@@ -185,10 +127,7 @@
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i
|
||||
data-lucide="shield"
|
||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
||||
></i>
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">
|
||||
@@ -197,10 +136,7 @@
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i
|
||||
data-lucide="badge-dollar-sign"
|
||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
||||
></i>
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">
|
||||
@@ -209,10 +145,7 @@
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i
|
||||
data-lucide="infinity"
|
||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
||||
></i>
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">
|
||||
@@ -221,20 +154,14 @@
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i
|
||||
data-lucide="layers"
|
||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
||||
></i>
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i
|
||||
data-lucide="zap"
|
||||
class="w-10 h-10 text-indigo-400 flex-shrink-0"
|
||||
></i>
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">
|
||||
@@ -259,47 +186,30 @@
|
||||
<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>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="search-bar"
|
||||
<input 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"
|
||||
placeholder="Search for a tool (e.g., 'split', 'organize'...)"
|
||||
/>
|
||||
<span
|
||||
class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2"
|
||||
>
|
||||
placeholder="Search for a tool (e.g., 'split', 'organize'...)" />
|
||||
<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>
|
||||
</span>
|
||||
</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>
|
||||
<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
|
||||
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"
|
||||
>
|
||||
<button
|
||||
id="back-to-grid"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"
|
||||
>
|
||||
<div 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">
|
||||
<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>
|
||||
<span class="cursor-pointer"> Back to Tools </span>
|
||||
</button>
|
||||
<div id="tool-content"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="loader-modal"
|
||||
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 id="loader-modal" 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>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">
|
||||
Processing...
|
||||
@@ -307,48 +217,33 @@
|
||||
</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"
|
||||
>
|
||||
<div
|
||||
class="bg-gray-800 max-w-sm w-full p-6 rounded-lg border border-gray-700 shadow-xl text-center"
|
||||
>
|
||||
<div id="alert-modal" 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">
|
||||
Alert
|
||||
</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6 whitespace-pre-line">
|
||||
This is an alert message.
|
||||
</p>
|
||||
<button
|
||||
id="alert-ok"
|
||||
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700"
|
||||
>
|
||||
<button id="alert-ok"
|
||||
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="preview-modal"
|
||||
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 id="preview-modal"
|
||||
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="p-4 border-b flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold">Document Preview</h3>
|
||||
<div class="flex gap-4">
|
||||
<button
|
||||
id="preview-download-btn"
|
||||
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700"
|
||||
>
|
||||
<button id="preview-download-btn"
|
||||
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">
|
||||
Download as PDF
|
||||
</button>
|
||||
<button
|
||||
id="preview-close-btn"
|
||||
class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700"
|
||||
>
|
||||
<button id="preview-close-btn"
|
||||
class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
@@ -362,96 +257,62 @@
|
||||
<!-- COMPLIANCE SECTION START -->
|
||||
<section id="security-compliance-section" class="py-20 hide-section">
|
||||
<div class="mb-8 text-center">
|
||||
<h2
|
||||
class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance"
|
||||
>
|
||||
<h2 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
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<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>
|
||||
<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
|
||||
</span>
|
||||
<br class="hidden sm:block" />
|
||||
<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
|
||||
</span>
|
||||
by following global security standards.
|
||||
</h2>
|
||||
</div>
|
||||
<div class="mb-16 text-center">
|
||||
<span
|
||||
class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors"
|
||||
>
|
||||
<span class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors">
|
||||
All the processing happens locally on your device.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4"
|
||||
>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4">
|
||||
<!-- GDPR Compliance -->
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div
|
||||
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"
|
||||
/>
|
||||
<div 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" />
|
||||
</div>
|
||||
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
|
||||
GDPR compliance
|
||||
</h3>
|
||||
<p
|
||||
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
|
||||
>
|
||||
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
|
||||
Protects the personal data and privacy of individuals within the
|
||||
European Union.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div
|
||||
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"
|
||||
/>
|
||||
<div 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" />
|
||||
</div>
|
||||
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
|
||||
CCPA compliance
|
||||
</h3>
|
||||
<p
|
||||
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
|
||||
>
|
||||
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
|
||||
Gives California residents rights over how their personal
|
||||
information is collected, used, and shared.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div
|
||||
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"
|
||||
/>
|
||||
<div 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" />
|
||||
</div>
|
||||
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
|
||||
HIPAA compliance
|
||||
</h3>
|
||||
<p
|
||||
class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
|
||||
>
|
||||
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
|
||||
Sets safeguards for handling sensitive health information in the
|
||||
United States healthcare system.
|
||||
</p>
|
||||
@@ -464,27 +325,16 @@
|
||||
<div class="section-divider hide-section"></div>
|
||||
|
||||
<section id="faq-accordion" class="space-y-4 hide-section">
|
||||
<h2
|
||||
class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8"
|
||||
>
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8">
|
||||
Frequently Asked <span class="marker-slanted">Questions</span>
|
||||
</h2>
|
||||
<!-- Existing FAQs here... -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button
|
||||
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>
|
||||
<button 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>
|
||||
</button>
|
||||
<div
|
||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Yes, absolutely. All tools on BentoPDF are 100% free to use, with
|
||||
no file limits, no sign-ups, and no watermarks. We believe
|
||||
@@ -495,20 +345,11 @@
|
||||
</div>
|
||||
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button
|
||||
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>
|
||||
<button 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>
|
||||
</button>
|
||||
<div
|
||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Your files are as secure as possible because they **never leave
|
||||
your computer**. All processing happens directly in your web
|
||||
@@ -519,20 +360,11 @@
|
||||
</div>
|
||||
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button
|
||||
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>
|
||||
<button 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>
|
||||
</button>
|
||||
<div
|
||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Yes! Since BentoPDF runs entirely in your browser, it works on any
|
||||
operating system with a modern web browser, including Windows,
|
||||
@@ -543,20 +375,11 @@
|
||||
|
||||
<!-- New FAQ: GDPR Compliance -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button
|
||||
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>
|
||||
<button 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>
|
||||
</button>
|
||||
<div
|
||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Yes. BentoPDF is fully GDPR compliant. Since all file processing
|
||||
happens locally in your browser and we never collect or transmit
|
||||
@@ -568,20 +391,11 @@
|
||||
|
||||
<!-- New FAQ: Data Storage -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button
|
||||
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>
|
||||
<button 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>
|
||||
</button>
|
||||
<div
|
||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
No. We never store, track, or log your files. Everything you do on
|
||||
BentoPDF happens in your browser memory and disappears once you
|
||||
@@ -593,20 +407,11 @@
|
||||
|
||||
<!-- New FAQ: Privacy -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button
|
||||
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>
|
||||
<button 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>
|
||||
</button>
|
||||
<div
|
||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Most PDF tools upload your files to a server for processing.
|
||||
BentoPDF never does that. We use secure, modern web technology to
|
||||
@@ -618,20 +423,11 @@
|
||||
|
||||
<!-- New FAQ: Browser-Based -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button
|
||||
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>
|
||||
<button 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>
|
||||
</button>
|
||||
<div
|
||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
By running entirely inside your browser, BentoPDF ensures that
|
||||
your files never leave your device. This eliminates the risks of
|
||||
@@ -643,30 +439,16 @@
|
||||
|
||||
<!-- New FAQ: Analytics -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button
|
||||
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>
|
||||
<button 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>
|
||||
</button>
|
||||
<div
|
||||
class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
We care about your privacy. BentoPDF does not track personal
|
||||
information. We use
|
||||
<a
|
||||
href="https://simpleanalytics.com"
|
||||
class="text-indigo-400 hover:underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Simple Analytics</a
|
||||
>
|
||||
<a href="https://simpleanalytics.com" 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
|
||||
many users visit our site, but
|
||||
<strong>we never know who you are</strong>. Simple Analytics is
|
||||
@@ -682,9 +464,7 @@
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
|
||||
What Our <span class="marker-slanted">Users</span> Say
|
||||
</h2>
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto"
|
||||
>
|
||||
<div 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="flex items-center mb-4">
|
||||
<div>
|
||||
@@ -781,9 +561,7 @@
|
||||
|
||||
<div class="section-divider"></div>
|
||||
<section id="support-section" class="py-20">
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||
Like My Work?
|
||||
</h2>
|
||||
@@ -793,12 +571,8 @@
|
||||
supporting its development. Every coffee helps!
|
||||
</p>
|
||||
|
||||
<a
|
||||
href="https://ko-fi.com/alio01"
|
||||
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"
|
||||
>
|
||||
<a href="https://ko-fi.com/alio01" 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>
|
||||
<span>Buy Me a Coffee</span>
|
||||
</a>
|
||||
@@ -810,16 +584,10 @@
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||
>
|
||||
<div 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="flex items-center justify-center md:justify-start mb-4">
|
||||
<img
|
||||
src="public/images/favicon.svg"
|
||||
alt="Bento PDF Logo"
|
||||
class="h-10 w-10 mr-3"
|
||||
/>
|
||||
<img 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>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
@@ -834,17 +602,13 @@
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="./about.html" class="hover:text-indigo-400"
|
||||
>About Us</a
|
||||
>
|
||||
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./contact.html" class="hover:text-indigo-400"
|
||||
>Contact Us</a
|
||||
>
|
||||
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -853,14 +617,10 @@
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="./terms.html" class="hover:text-indigo-400"
|
||||
>Terms and Conditions</a
|
||||
>
|
||||
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||
>Privacy Policy</a
|
||||
>
|
||||
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -868,72 +628,33 @@
|
||||
<div>
|
||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||
<div class="flex justify-center md:justify-start space-x-4">
|
||||
<a
|
||||
href="https://github.com/alam00000/bentopdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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"
|
||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||
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"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/q42xWQmJ"
|
||||
target="_blank"
|
||||
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"
|
||||
>
|
||||
<a href="https://discord.gg/q42xWQmJ" target="_blank" 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
|
||||
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>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.instagram.com/thebentopdf/"
|
||||
class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram"
|
||||
>
|
||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="Instagram">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/company/bentopdf/"
|
||||
class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||
title="LinkedIn">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://x.com/BentoPDF"
|
||||
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"
|
||||
>
|
||||
<a href="https://x.com/BentoPDF" 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
|
||||
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>
|
||||
</a>
|
||||
</div>
|
||||
@@ -946,4 +667,5 @@
|
||||
<script type="module" src="src/js/main.ts"></script>
|
||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -510,3 +510,14 @@ details > summary .icon {
|
||||
details[open] > summary .icon {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
button,
|
||||
.btn,
|
||||
.btn-gradient {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
.btn:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -3,6 +3,12 @@ export const categories = [
|
||||
{
|
||||
name: 'Popular Tools',
|
||||
tools: [
|
||||
{
|
||||
href: '/src/pages/pdf-multi-tool.html',
|
||||
name: 'PDF Multi Tool',
|
||||
icon: 'pencil-ruler',
|
||||
subtitle: 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.',
|
||||
},
|
||||
{
|
||||
id: 'merge',
|
||||
name: 'Merge PDF',
|
||||
@@ -312,22 +318,23 @@ export const categories = [
|
||||
icon: 'paperclip',
|
||||
subtitle: 'Embed one or more files into your PDF.',
|
||||
},
|
||||
{
|
||||
id: 'extract-attachments',
|
||||
name: 'Extract Attachments',
|
||||
icon: 'download',
|
||||
subtitle: 'Extract all embedded files from PDF(s) as a ZIP.',
|
||||
},
|
||||
{
|
||||
id: 'edit-attachments',
|
||||
name: 'Edit Attachments',
|
||||
icon: 'file-edit',
|
||||
subtitle: 'View, remove, or replace attachments in your PDF.',
|
||||
},
|
||||
// TODO@ALAM - MAKE THIS LATER, ONCE INTEGERATED WITH CPDF
|
||||
// {
|
||||
// id: 'extract-attachments',
|
||||
// name: 'Extract Attachments',
|
||||
// icon: 'download',
|
||||
// subtitle: 'Extract all embedded files from PDF(s) as a ZIP.',
|
||||
// },
|
||||
// {
|
||||
// id: 'edit-attachments',
|
||||
// name: 'Edit Attachments',
|
||||
// icon: 'file-edit',
|
||||
// subtitle: 'View, remove, or replace attachments in your PDF.',
|
||||
// },
|
||||
{
|
||||
href: '/src/pages/pdf-multi-tool.html',
|
||||
name: 'PDF Multi Tool',
|
||||
icon: 'layers',
|
||||
icon: 'pencil-ruler',
|
||||
subtitle: 'Full-featured PDF editor with page management.',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,205 +1,207 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
// TODO@ALAM - USE CPDF HERE
|
||||
|
||||
let currentAttachments: Array<{ name: string; index: number; size: number }> = [];
|
||||
let attachmentsToRemove: Set<number> = new Set();
|
||||
let attachmentsToReplace: Map<number, File> = new Map();
|
||||
// import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
// import { state } from '../state.js';
|
||||
// import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function setupEditAttachmentsTool() {
|
||||
const optionsDiv = document.getElementById('edit-attachments-options');
|
||||
if (!optionsDiv || !state.pdfDoc) return;
|
||||
// let currentAttachments: Array<{ name: string; index: number; size: number }> = [];
|
||||
// let attachmentsToRemove: Set<number> = new Set();
|
||||
// let attachmentsToReplace: Map<number, File> = new Map();
|
||||
|
||||
optionsDiv.classList.remove('hidden');
|
||||
await loadAttachmentsList();
|
||||
}
|
||||
// export async function setupEditAttachmentsTool() {
|
||||
// const optionsDiv = document.getElementById('edit-attachments-options');
|
||||
// if (!optionsDiv || !state.pdfDoc) return;
|
||||
|
||||
async function loadAttachmentsList() {
|
||||
const attachmentsList = document.getElementById('attachments-list');
|
||||
if (!attachmentsList || !state.pdfDoc) return;
|
||||
// optionsDiv.classList.remove('hidden');
|
||||
// await loadAttachmentsList();
|
||||
// }
|
||||
|
||||
attachmentsList.innerHTML = '';
|
||||
currentAttachments = [];
|
||||
attachmentsToRemove.clear();
|
||||
attachmentsToReplace.clear();
|
||||
// async function loadAttachmentsList() {
|
||||
// const attachmentsList = document.getElementById('attachments-list');
|
||||
// if (!attachmentsList || !state.pdfDoc) return;
|
||||
|
||||
try {
|
||||
// Get embedded files from PDF
|
||||
const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
|
||||
.filter(([ref, obj]: any) => {
|
||||
const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
|
||||
return dict && dict.get('Type')?.toString() === '/Filespec';
|
||||
});
|
||||
// attachmentsList.innerHTML = '';
|
||||
// currentAttachments = [];
|
||||
// attachmentsToRemove.clear();
|
||||
// attachmentsToReplace.clear();
|
||||
|
||||
if (embeddedFiles.length === 0) {
|
||||
attachmentsList.innerHTML = '<p class="text-gray-400 text-center py-4">No attachments found in this PDF.</p>';
|
||||
return;
|
||||
}
|
||||
// try {
|
||||
// // Get embedded files from PDF
|
||||
// const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
|
||||
// .filter(([ref, obj]: any) => {
|
||||
// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
|
||||
// return dict && dict.get('Type')?.toString() === '/Filespec';
|
||||
// });
|
||||
|
||||
let index = 0;
|
||||
for (const [ref, fileSpec] of embeddedFiles) {
|
||||
try {
|
||||
const fileSpecDict = fileSpec as any;
|
||||
const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||
fileSpecDict.get('F')?.decodeText() ||
|
||||
`attachment-${index + 1}`;
|
||||
// if (embeddedFiles.length === 0) {
|
||||
// attachmentsList.innerHTML = '<p class="text-gray-400 text-center py-4">No attachments found in this PDF.</p>';
|
||||
// return;
|
||||
// }
|
||||
|
||||
const ef = fileSpecDict.get('EF');
|
||||
let fileSize = 0;
|
||||
if (ef) {
|
||||
const fRef = ef.get('F') || ef.get('UF');
|
||||
if (fRef) {
|
||||
const fileStream = state.pdfDoc.context.lookup(fRef);
|
||||
if (fileStream) {
|
||||
fileSize = (fileStream as any).getContents().length;
|
||||
}
|
||||
}
|
||||
}
|
||||
// let index = 0;
|
||||
// for (const [ref, fileSpec] of embeddedFiles) {
|
||||
// try {
|
||||
// const fileSpecDict = fileSpec as any;
|
||||
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||
// fileSpecDict.get('F')?.decodeText() ||
|
||||
// `attachment-${index + 1}`;
|
||||
|
||||
currentAttachments.push({ name: fileName, index, size: fileSize });
|
||||
// const ef = fileSpecDict.get('EF');
|
||||
// let fileSize = 0;
|
||||
// if (ef) {
|
||||
// const fRef = ef.get('F') || ef.get('UF');
|
||||
// if (fRef) {
|
||||
// const fileStream = state.pdfDoc.context.lookup(fRef);
|
||||
// if (fileStream) {
|
||||
// fileSize = (fileStream as any).getContents().length;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const attachmentDiv = document.createElement('div');
|
||||
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
||||
attachmentDiv.dataset.attachmentIndex = index.toString();
|
||||
// currentAttachments.push({ name: fileName, index, size: fileSize });
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'flex-1';
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'text-white font-medium block';
|
||||
nameSpan.textContent = fileName;
|
||||
const sizeSpan = document.createElement('span');
|
||||
sizeSpan.className = 'text-gray-400 text-sm';
|
||||
sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
|
||||
infoDiv.append(nameSpan, sizeSpan);
|
||||
// const attachmentDiv = document.createElement('div');
|
||||
// attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
|
||||
// attachmentDiv.dataset.attachmentIndex = index.toString();
|
||||
|
||||
const actionsDiv = document.createElement('div');
|
||||
actionsDiv.className = 'flex items-center gap-2';
|
||||
// const infoDiv = document.createElement('div');
|
||||
// infoDiv.className = 'flex-1';
|
||||
// const nameSpan = document.createElement('span');
|
||||
// nameSpan.className = 'text-white font-medium block';
|
||||
// nameSpan.textContent = fileName;
|
||||
// const sizeSpan = document.createElement('span');
|
||||
// sizeSpan.className = 'text-gray-400 text-sm';
|
||||
// sizeSpan.textContent = `${Math.round(fileSize / 1024)} KB`;
|
||||
// infoDiv.append(nameSpan, sizeSpan);
|
||||
|
||||
// Remove button
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm';
|
||||
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
removeBtn.title = 'Remove attachment';
|
||||
removeBtn.onclick = () => {
|
||||
attachmentsToRemove.add(index);
|
||||
attachmentDiv.classList.add('opacity-50', 'line-through');
|
||||
removeBtn.disabled = true;
|
||||
};
|
||||
// const actionsDiv = document.createElement('div');
|
||||
// actionsDiv.className = 'flex items-center gap-2';
|
||||
|
||||
// Replace button
|
||||
const replaceBtn = document.createElement('button');
|
||||
replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm';
|
||||
replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>';
|
||||
replaceBtn.title = 'Replace attachment';
|
||||
replaceBtn.onclick = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
attachmentsToReplace.set(index, file);
|
||||
nameSpan.textContent = `${fileName} → ${file.name}`;
|
||||
nameSpan.classList.add('text-yellow-400');
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
// // Remove button
|
||||
// const removeBtn = document.createElement('button');
|
||||
// removeBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded text-sm';
|
||||
// removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
|
||||
// removeBtn.title = 'Remove attachment';
|
||||
// removeBtn.onclick = () => {
|
||||
// attachmentsToRemove.add(index);
|
||||
// attachmentDiv.classList.add('opacity-50', 'line-through');
|
||||
// removeBtn.disabled = true;
|
||||
// };
|
||||
|
||||
actionsDiv.append(replaceBtn, removeBtn);
|
||||
attachmentDiv.append(infoDiv, actionsDiv);
|
||||
attachmentsList.appendChild(attachmentDiv);
|
||||
index++;
|
||||
} catch (e) {
|
||||
console.warn(`Failed to process attachment ${index}:`, e);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading attachments:', e);
|
||||
showAlert('Error', 'Failed to load attachments from PDF.');
|
||||
}
|
||||
}
|
||||
// // Replace button
|
||||
// const replaceBtn = document.createElement('button');
|
||||
// replaceBtn.className = 'btn bg-indigo-600 hover:bg-indigo-700 text-white px-3 py-1 rounded text-sm';
|
||||
// replaceBtn.innerHTML = '<i data-lucide="refresh-cw" class="w-4 h-4"></i>';
|
||||
// replaceBtn.title = 'Replace attachment';
|
||||
// replaceBtn.onclick = () => {
|
||||
// const input = document.createElement('input');
|
||||
// input.type = 'file';
|
||||
// input.onchange = async (e) => {
|
||||
// const file = (e.target as HTMLInputElement).files?.[0];
|
||||
// if (file) {
|
||||
// attachmentsToReplace.set(index, file);
|
||||
// nameSpan.textContent = `${fileName} → ${file.name}`;
|
||||
// nameSpan.classList.add('text-yellow-400');
|
||||
// }
|
||||
// };
|
||||
// input.click();
|
||||
// };
|
||||
|
||||
export async function editAttachments() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF is not loaded.');
|
||||
return;
|
||||
}
|
||||
// actionsDiv.append(replaceBtn, removeBtn);
|
||||
// attachmentDiv.append(infoDiv, actionsDiv);
|
||||
// attachmentsList.appendChild(attachmentDiv);
|
||||
// index++;
|
||||
// } catch (e) {
|
||||
// console.warn(`Failed to process attachment ${index}:`, e);
|
||||
// index++;
|
||||
// }
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error('Error loading attachments:', e);
|
||||
// showAlert('Error', 'Failed to load attachments from PDF.');
|
||||
// }
|
||||
// }
|
||||
|
||||
showLoader('Updating attachments...');
|
||||
try {
|
||||
// Create a new PDF document
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
// export async function editAttachments() {
|
||||
// if (!state.pdfDoc) {
|
||||
// showAlert('Error', 'PDF is not loaded.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Copy all pages
|
||||
const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices());
|
||||
pages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||
// showLoader('Updating attachments...');
|
||||
// try {
|
||||
// // Create a new PDF document
|
||||
// const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
// Handle attachments
|
||||
const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
|
||||
.filter(([ref, obj]: any) => {
|
||||
const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
|
||||
return dict && dict.get('Type')?.toString() === '/Filespec';
|
||||
});
|
||||
// // Copy all pages
|
||||
// const pages = await newPdfDoc.copyPages(state.pdfDoc, state.pdfDoc.getPageIndices());
|
||||
// pages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||
|
||||
let attachmentIndex = 0;
|
||||
for (const [ref, fileSpec] of embeddedFiles) {
|
||||
if (attachmentsToRemove.has(attachmentIndex)) {
|
||||
attachmentIndex++;
|
||||
continue; // Skip removed attachments
|
||||
}
|
||||
// // Handle attachments
|
||||
// const embeddedFiles = state.pdfDoc.context.enumerateIndirectObjects()
|
||||
// .filter(([ref, obj]: any) => {
|
||||
// const dict = obj instanceof PDFLibDocument.context.dict ? obj : null;
|
||||
// return dict && dict.get('Type')?.toString() === '/Filespec';
|
||||
// });
|
||||
|
||||
if (attachmentsToReplace.has(attachmentIndex)) {
|
||||
// Replace attachment
|
||||
const replacementFile = attachmentsToReplace.get(attachmentIndex)!;
|
||||
const fileBytes = await readFileAsArrayBuffer(replacementFile);
|
||||
await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, {
|
||||
mimeType: replacementFile.type || 'application/octet-stream',
|
||||
description: `Attached file: ${replacementFile.name}`,
|
||||
creationDate: new Date(),
|
||||
modificationDate: new Date(replacementFile.lastModified),
|
||||
});
|
||||
} else {
|
||||
// Keep existing attachment - copy it
|
||||
try {
|
||||
const fileSpecDict = fileSpec as any;
|
||||
const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||
fileSpecDict.get('F')?.decodeText() ||
|
||||
`attachment-${attachmentIndex + 1}`;
|
||||
// let attachmentIndex = 0;
|
||||
// for (const [ref, fileSpec] of embeddedFiles) {
|
||||
// if (attachmentsToRemove.has(attachmentIndex)) {
|
||||
// attachmentIndex++;
|
||||
// continue; // Skip removed attachments
|
||||
// }
|
||||
|
||||
const ef = fileSpecDict.get('EF');
|
||||
if (ef) {
|
||||
const fRef = ef.get('F') || ef.get('UF');
|
||||
if (fRef) {
|
||||
const fileStream = state.pdfDoc.context.lookup(fRef);
|
||||
if (fileStream) {
|
||||
const fileData = (fileStream as any).getContents();
|
||||
await newPdfDoc.attach(fileData, fileName, {
|
||||
mimeType: 'application/octet-stream',
|
||||
description: `Attached file: ${fileName}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to copy attachment ${attachmentIndex}:`, e);
|
||||
}
|
||||
}
|
||||
attachmentIndex++;
|
||||
}
|
||||
// if (attachmentsToReplace.has(attachmentIndex)) {
|
||||
// // Replace attachment
|
||||
// const replacementFile = attachmentsToReplace.get(attachmentIndex)!;
|
||||
// const fileBytes = await readFileAsArrayBuffer(replacementFile);
|
||||
// await newPdfDoc.attach(fileBytes as ArrayBuffer, replacementFile.name, {
|
||||
// mimeType: replacementFile.type || 'application/octet-stream',
|
||||
// description: `Attached file: ${replacementFile.name}`,
|
||||
// creationDate: new Date(),
|
||||
// modificationDate: new Date(replacementFile.lastModified),
|
||||
// });
|
||||
// } else {
|
||||
// // Keep existing attachment - copy it
|
||||
// try {
|
||||
// const fileSpecDict = fileSpec as any;
|
||||
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||
// fileSpecDict.get('F')?.decodeText() ||
|
||||
// `attachment-${attachmentIndex + 1}`;
|
||||
|
||||
const pdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
`edited-attachments-${state.files[0].name}`
|
||||
);
|
||||
showAlert('Success', 'Attachments updated successfully!');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to edit attachments.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
// const ef = fileSpecDict.get('EF');
|
||||
// if (ef) {
|
||||
// const fRef = ef.get('F') || ef.get('UF');
|
||||
// if (fRef) {
|
||||
// const fileStream = state.pdfDoc.context.lookup(fRef);
|
||||
// if (fileStream) {
|
||||
// const fileData = (fileStream as any).getContents();
|
||||
// await newPdfDoc.attach(fileData, fileName, {
|
||||
// mimeType: 'application/octet-stream',
|
||||
// description: `Attached file: ${fileName}`,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.warn(`Failed to copy attachment ${attachmentIndex}:`, e);
|
||||
// }
|
||||
// }
|
||||
// attachmentIndex++;
|
||||
// }
|
||||
|
||||
// const pdfBytes = await newPdfDoc.save();
|
||||
// downloadFile(
|
||||
// new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||
// `edited-attachments-${state.files[0].name}`
|
||||
// );
|
||||
// showAlert('Success', 'Attachments updated successfully!');
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// showAlert('Error', 'Failed to edit attachments.');
|
||||
// } finally {
|
||||
// hideLoader();
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
@@ -1,86 +1,88 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import JSZip from 'jszip';
|
||||
// TODO@ALAM - USE CPDF HERE
|
||||
|
||||
export async function extractAttachments() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
// import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
// import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
// import { state } from '../state.js';
|
||||
// import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
// import JSZip from 'jszip';
|
||||
|
||||
showLoader('Extracting attachments...');
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
let totalAttachments = 0;
|
||||
// export async function extractAttachments() {
|
||||
// if (state.files.length === 0) {
|
||||
// showAlert('No Files', 'Please select at least one PDF file.');
|
||||
// return;
|
||||
// }
|
||||
|
||||
for (const file of state.files) {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||
ignoreEncryption: true,
|
||||
});
|
||||
// showLoader('Extracting attachments...');
|
||||
// try {
|
||||
// const zip = new JSZip();
|
||||
// let totalAttachments = 0;
|
||||
|
||||
const embeddedFiles = pdfDoc.context.enumerateIndirectObjects()
|
||||
.filter(([ref, obj]: any) => {
|
||||
// obj must be a PDFDict
|
||||
if (obj && typeof obj.get === 'function') {
|
||||
const type = obj.get('Type');
|
||||
return type && type.toString() === '/Filespec';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
// for (const file of state.files) {
|
||||
// const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
// const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||
// ignoreEncryption: true,
|
||||
// });
|
||||
|
||||
if (embeddedFiles.length === 0) {
|
||||
console.warn(`No attachments found in ${file.name}`);
|
||||
continue;
|
||||
}
|
||||
// const embeddedFiles = pdfDoc.context.enumerateIndirectObjects()
|
||||
// .filter(([ref, obj]: any) => {
|
||||
// // obj must be a PDFDict
|
||||
// if (obj && typeof obj.get === 'function') {
|
||||
// const type = obj.get('Type');
|
||||
// return type && type.toString() === '/Filespec';
|
||||
// }
|
||||
// return false;
|
||||
// });
|
||||
|
||||
// Extract attachments
|
||||
const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
for (let i = 0; i < embeddedFiles.length; i++) {
|
||||
try {
|
||||
const [ref, fileSpec] = embeddedFiles[i];
|
||||
const fileSpecDict = fileSpec as any;
|
||||
// if (embeddedFiles.length === 0) {
|
||||
// console.warn(`No attachments found in ${file.name}`);
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// Get attachment name
|
||||
const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||
fileSpecDict.get('F')?.decodeText() ||
|
||||
`attachment-${i + 1}`;
|
||||
// // Extract attachments
|
||||
// const baseName = file.name.replace(/\.pdf$/i, '');
|
||||
// for (let i = 0; i < embeddedFiles.length; i++) {
|
||||
// try {
|
||||
// const [ref, fileSpec] = embeddedFiles[i];
|
||||
// const fileSpecDict = fileSpec as any;
|
||||
|
||||
// Get embedded file stream
|
||||
const ef = fileSpecDict.get('EF');
|
||||
if (ef) {
|
||||
const fRef = ef.get('F') || ef.get('UF');
|
||||
if (fRef) {
|
||||
const fileStream = pdfDoc.context.lookup(fRef);
|
||||
if (fileStream) {
|
||||
const fileData = (fileStream as any).getContents();
|
||||
zip.file(`${baseName}_${fileName}`, fileData);
|
||||
totalAttachments++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// // Get attachment name
|
||||
// const fileName = fileSpecDict.get('UF')?.decodeText() ||
|
||||
// fileSpecDict.get('F')?.decodeText() ||
|
||||
// `attachment-${i + 1}`;
|
||||
|
||||
if (totalAttachments === 0) {
|
||||
showAlert('No Attachments', 'No attachments were found in the selected PDF(s).');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
// // Get embedded file stream
|
||||
// const ef = fileSpecDict.get('EF');
|
||||
// if (ef) {
|
||||
// const fRef = ef.get('F') || ef.get('UF');
|
||||
// if (fRef) {
|
||||
// const fileStream = pdfDoc.context.lookup(fRef);
|
||||
// if (fileStream) {
|
||||
// const fileData = (fileStream as any).getContents();
|
||||
// zip.file(`${baseName}_${fileName}`, fileData);
|
||||
// totalAttachments++;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.warn(`Failed to extract attachment ${i} from ${file.name}:`, e);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'extracted-attachments.zip');
|
||||
showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
// if (totalAttachments === 0) {
|
||||
// showAlert('No Attachments', 'No attachments were found in the selected PDF(s).');
|
||||
// hideLoader();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
// downloadFile(zipBlob, 'extracted-attachments.zip');
|
||||
// showAlert('Success', `Extracted ${totalAttachments} attachment(s) successfully!`);
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// showAlert('Error', 'Failed to extract attachments. The PDF may not contain attachments or may be corrupted.');
|
||||
// } finally {
|
||||
// hideLoader();
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
@@ -63,8 +63,8 @@ import {
|
||||
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
||||
import { linearizePdf } from './linearize.js';
|
||||
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
|
||||
import { extractAttachments } from './extract-attachments.js';
|
||||
import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
|
||||
// import { extractAttachments } from './extract-attachments.js';
|
||||
// import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
|
||||
import { sanitizePdf } from './sanitize-pdf.js';
|
||||
import { removeRestrictions } from './remove-restrictions.js';
|
||||
|
||||
@@ -140,10 +140,10 @@ export const toolLogic = {
|
||||
process: addAttachments,
|
||||
setup: setupAddAttachmentsTool,
|
||||
},
|
||||
'extract-attachments': extractAttachments,
|
||||
'edit-attachments': {
|
||||
process: editAttachments,
|
||||
setup: setupEditAttachmentsTool,
|
||||
},
|
||||
// 'extract-attachments': extractAttachments,
|
||||
// 'edit-attachments': {
|
||||
// process: editAttachments,
|
||||
// setup: setupEditAttachmentsTool,
|
||||
// },
|
||||
'sanitize-pdf': sanitizePdf,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
// @TODO:@ALAM- sometimes I think... and then I forget...
|
||||
//
|
||||
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import JSZip from 'jszip';
|
||||
import Sortable from 'sortablejs';
|
||||
import { downloadFile } from '../utils/helpers';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -34,7 +38,7 @@ const redoStack: Snapshot[] = [];
|
||||
|
||||
function snapshot() {
|
||||
const snap: Snapshot = {
|
||||
allPages: allPages.map(p => ({ ...p })),
|
||||
allPages: allPages.map(p => ({ ...p, canvas: p.canvas })),
|
||||
selectedPages: Array.from(selectedPages),
|
||||
splitMarkers: Array.from(splitMarkers),
|
||||
};
|
||||
@@ -43,7 +47,10 @@ function snapshot() {
|
||||
}
|
||||
|
||||
function restore(snap: Snapshot) {
|
||||
allPages = snap.allPages.map(p => ({ ...p }));
|
||||
allPages = snap.allPages.map(p => ({
|
||||
...p,
|
||||
canvas: p.canvas
|
||||
}));
|
||||
selectedPages = new Set(snap.selectedPages);
|
||||
splitMarkers = new Set(snap.splitMarkers);
|
||||
updatePageDisplay();
|
||||
@@ -202,7 +209,6 @@ function initializeTool() {
|
||||
}
|
||||
});
|
||||
|
||||
// Modal close button
|
||||
document.getElementById('modal-close-btn')?.addEventListener('click', hideModal);
|
||||
document.getElementById('modal')?.addEventListener('click', (e) => {
|
||||
if (e.target === document.getElementById('modal')) {
|
||||
@@ -210,7 +216,6 @@ function initializeTool() {
|
||||
}
|
||||
});
|
||||
|
||||
// Drag and drop
|
||||
const uploadArea = document.getElementById('upload-area');
|
||||
if (uploadArea) {
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
@@ -230,7 +235,6 @@ function initializeTool() {
|
||||
});
|
||||
}
|
||||
|
||||
// Show upload area initially
|
||||
document.getElementById('upload-area')?.classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -315,7 +319,6 @@ async function loadPdfs(files: File[]) {
|
||||
}
|
||||
|
||||
function getCacheKey(pdfIndex: number, pageIndex: number): string {
|
||||
// Removed rotation from cache key - canvas is always rendered at 0 degrees
|
||||
return `${pdfIndex}-${pageIndex}`;
|
||||
}
|
||||
|
||||
@@ -330,12 +333,10 @@ async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: n
|
||||
if (pageCanvasCache.has(cacheKey)) {
|
||||
canvas = pageCanvasCache.get(cacheKey)!;
|
||||
} else {
|
||||
// Render page preview at 0 degrees rotation using pdfjs
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
|
||||
const page = await pdf.getPage(pageIndex + 1);
|
||||
|
||||
// Always render at 0 rotation - visual rotation is applied via CSS
|
||||
const viewport = page.getViewport({ scale: 0.5, rotation: 0 });
|
||||
|
||||
canvas = document.createElement('canvas');
|
||||
@@ -384,20 +385,14 @@ function createPageCard(pageData: PageData, index: number) {
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
|
||||
preview.style.minHeight = '160px';
|
||||
preview.style.maxHeight = '256px';
|
||||
preview.style.height = '250px';
|
||||
|
||||
const previewCanvas = pageData.canvas;
|
||||
previewCanvas.className = 'max-w-full max-h-full object-contain';
|
||||
|
||||
// Apply visual rotation using CSS transform
|
||||
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
||||
|
||||
// Adjust container dimensions based on rotation
|
||||
if (pageData.visualRotation === 90 || pageData.visualRotation === 270) {
|
||||
preview.style.aspectRatio = `${previewCanvas.height} / ${previewCanvas.width}`;
|
||||
} else {
|
||||
preview.style.aspectRatio = `${previewCanvas.width} / ${previewCanvas.height}`;
|
||||
}
|
||||
previewCanvas.style.transition = 'transform 0.2s ease';
|
||||
|
||||
preview.appendChild(previewCanvas);
|
||||
|
||||
@@ -408,7 +403,11 @@ function createPageCard(pageData: PageData, index: number) {
|
||||
|
||||
// Actions toolbar
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity';
|
||||
actions.className = 'flex items-center justify-center gap-1 sm:opacity-0 group-hover:opacity-100 transition-opacity absolute bottom-2 left-0 right-0';
|
||||
|
||||
const actionsInner = document.createElement('div');
|
||||
actionsInner.className = 'flex items-center gap-1 bg-gray-900/90 rounded px-2 py-1';
|
||||
actions.appendChild(actionsInner);
|
||||
|
||||
// Select checkbox
|
||||
const selectBtn = document.createElement('button');
|
||||
@@ -441,6 +440,7 @@ function createPageCard(pageData: PageData, index: number) {
|
||||
const duplicateBtn = document.createElement('button');
|
||||
duplicateBtn.className = 'p-1 rounded hover:bg-gray-700';
|
||||
duplicateBtn.innerHTML = '<i data-lucide="copy" class="w-4 h-4 text-gray-300"></i>';
|
||||
duplicateBtn.title = 'Duplicate this page';
|
||||
duplicateBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
snapshot();
|
||||
@@ -451,6 +451,7 @@ function createPageCard(pageData: PageData, index: number) {
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'p-1 rounded hover:bg-gray-700';
|
||||
deleteBtn.innerHTML = '<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();
|
||||
@@ -480,7 +481,7 @@ function createPageCard(pageData: PageData, index: number) {
|
||||
renderSplitMarkers();
|
||||
};
|
||||
|
||||
actions.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
|
||||
actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
|
||||
card.append(preview, info, actions, selectBtn);
|
||||
pagesContainer.appendChild(card);
|
||||
|
||||
@@ -506,7 +507,6 @@ function setupSortable() {
|
||||
});
|
||||
}
|
||||
|
||||
// Optimized selection that only updates the specific card
|
||||
function toggleSelectOptimized(index: number) {
|
||||
if (selectedPages.has(index)) {
|
||||
selectedPages.delete(index);
|
||||
@@ -546,7 +546,6 @@ function deselectAll() {
|
||||
updatePageDisplay();
|
||||
}
|
||||
|
||||
// Instant rotation - just update visual rotation, no re-rendering
|
||||
function rotatePage(index: number, delta: number) {
|
||||
snapshot();
|
||||
|
||||
@@ -554,7 +553,6 @@ function rotatePage(index: number, delta: number) {
|
||||
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
|
||||
pageData.rotation = (pageData.rotation + delta + 360) % 360;
|
||||
|
||||
// Just update the specific card's transform
|
||||
const pagesContainer = document.getElementById('pages-container');
|
||||
if (!pagesContainer) return;
|
||||
|
||||
@@ -566,13 +564,7 @@ function rotatePage(index: number, delta: number) {
|
||||
|
||||
if (canvas && preview) {
|
||||
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
||||
|
||||
// Adjust container aspect ratio
|
||||
if (pageData.visualRotation === 90 || pageData.visualRotation === 270) {
|
||||
(preview as HTMLElement).style.aspectRatio = `${canvas.height} / ${canvas.width}`;
|
||||
} else {
|
||||
(preview as HTMLElement).style.aspectRatio = `${canvas.width} / ${canvas.height}`;
|
||||
}
|
||||
canvas.style.transition = 'transform 0.2s ease';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,7 +572,6 @@ function duplicatePage(index: number) {
|
||||
const originalPageData = allPages[index];
|
||||
const originalCanvas = originalPageData.canvas;
|
||||
|
||||
// Create a new canvas and copy content
|
||||
const newCanvas = document.createElement('canvas');
|
||||
newCanvas.width = originalCanvas.width;
|
||||
newCanvas.height = originalCanvas.height;
|
||||
@@ -603,13 +594,18 @@ function duplicatePage(index: number) {
|
||||
function deletePage(index: number) {
|
||||
allPages.splice(index, 1);
|
||||
selectedPages.delete(index);
|
||||
// Update selected indices
|
||||
const newSelected = new Set<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();
|
||||
}
|
||||
|
||||
@@ -640,7 +636,6 @@ async function handleInsertPdf(e: Event) {
|
||||
newPages.push(allPages.pop()!);
|
||||
}
|
||||
|
||||
// Insert pages after the specified index
|
||||
allPages.splice(insertAfterIndex + 1, 0, ...newPages);
|
||||
updatePageDisplay();
|
||||
} catch (e) {
|
||||
@@ -660,10 +655,8 @@ function renderSplitMarkers() {
|
||||
const pagesContainer = document.getElementById('pages-container');
|
||||
if (!pagesContainer) return;
|
||||
|
||||
// Remove all existing split markers
|
||||
pagesContainer.querySelectorAll('.split-marker').forEach(m => m.remove());
|
||||
|
||||
// Add split markers between cards
|
||||
Array.from(pagesContainer.children).forEach((cardEl, i) => {
|
||||
if (splitMarkers.has(i)) {
|
||||
const marker = document.createElement('div');
|
||||
@@ -675,7 +668,6 @@ function renderSplitMarkers() {
|
||||
}
|
||||
|
||||
function addBlankPage() {
|
||||
// Create a blank page
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 595;
|
||||
canvas.height = 842;
|
||||
@@ -699,7 +691,6 @@ function addBlankPage() {
|
||||
updatePageDisplay();
|
||||
}
|
||||
|
||||
// Instant bulk rotation - just update visual rotation
|
||||
function bulkRotate(delta: number) {
|
||||
if (selectedPages.size === 0) {
|
||||
showModal('No Selection', 'Please select pages to rotate.', 'info');
|
||||
@@ -712,7 +703,6 @@ function bulkRotate(delta: number) {
|
||||
pageData.rotation = (pageData.rotation + delta + 360) % 360;
|
||||
});
|
||||
|
||||
// Update display for all rotated pages
|
||||
updatePageDisplay();
|
||||
}
|
||||
|
||||
@@ -724,6 +714,12 @@ function bulkDelete() {
|
||||
const indices = Array.from(selectedPages).sort((a, b) => b - a);
|
||||
indices.forEach(index => allPages.splice(index, 1));
|
||||
selectedPages.clear();
|
||||
|
||||
if (allPages.length === 0) {
|
||||
resetAll();
|
||||
return;
|
||||
}
|
||||
|
||||
updatePageDisplay();
|
||||
}
|
||||
|
||||
@@ -742,11 +738,18 @@ function bulkDuplicate() {
|
||||
|
||||
function bulkSplit() {
|
||||
if (selectedPages.size === 0) {
|
||||
showModal('No Selection', 'Please select pages to split.', 'info');
|
||||
showModal('No Selection', 'Please select pages to mark for splitting.', 'info');
|
||||
return;
|
||||
}
|
||||
const indices = Array.from(selectedPages);
|
||||
downloadPagesAsPdf(indices, 'selected-pages.pdf');
|
||||
indices.forEach(index => {
|
||||
if (!splitMarkers.has(index)) {
|
||||
splitMarkers.add(index);
|
||||
}
|
||||
});
|
||||
renderSplitMarkers();
|
||||
selectedPages.clear();
|
||||
updatePageDisplay();
|
||||
}
|
||||
|
||||
async function bulkDownload() {
|
||||
@@ -759,9 +762,80 @@ async function bulkDownload() {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -785,12 +859,8 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
|
||||
|
||||
const pdfBytes = await newPdf.save();
|
||||
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
downloadFile(blob, filename);
|
||||
} catch (e) {
|
||||
console.error('Failed to create PDF:', e);
|
||||
showModal('Error', 'Failed to create PDF.', 'error');
|
||||
|
||||
@@ -4,6 +4,7 @@ import { setupToolInterface } from './handlers/toolSelectionHandler.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import '../css/styles.css';
|
||||
import { formatStars } from './utils/helpers.js';
|
||||
|
||||
const init = () => {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
@@ -274,7 +275,7 @@ const init = () => {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.stargazers_count !== undefined) {
|
||||
githubStarsElement.textContent = data.stargazers_count.toLocaleString();
|
||||
githubStarsElement.textContent = formatStars(data.stargazers_count);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -187,3 +187,10 @@ export function initializeIcons(): void {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function formatStars(num: number) {
|
||||
if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toLocaleString();
|
||||
};
|
||||
@@ -32,71 +32,89 @@
|
||||
<!-- Main Container -->
|
||||
<div class="flex flex-col h-[calc(100vh-4rem)]">
|
||||
<!-- Toolbar -->
|
||||
<div class="bg-gray-800 border-b border-gray-700 p-4 flex flex-wrap items-center gap-2 overflow-x-auto">
|
||||
<div class="bg-gray-800 border-b border-gray-700 p-2 sm:p-4 overflow-x-auto">
|
||||
<div class="flex flex-wrap items-center gap-1 sm:gap-2 min-w-max">
|
||||
<button id="upload-pdfs-btn"
|
||||
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2">
|
||||
<i data-lucide="upload" class="w-4 h-4"></i> Upload PDFs
|
||||
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-4 py-1.5 sm:py-2 rounded flex items-center gap-1 sm:gap-2 text-sm">
|
||||
<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-6 mx-2"></div>
|
||||
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||
<button id="add-blank-page-btn"
|
||||
class="flex items-center gap-1 btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm">
|
||||
<i data-lucide="file-plus" class="w-4 h-4"></i> Add Blank
|
||||
class="flex items-center gap-1 btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm">
|
||||
<i data-lucide="file-plus" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||
<span class="hidden sm:inline">Add Blank</span>
|
||||
</button>
|
||||
<div class="border-l border-gray-600 h-6 mx-2"></div>
|
||||
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||
<button id="undo-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Undo
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<i data-lucide="rotate-ccw" 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="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="rotate-cw" class="w-4 h-4"></i> Redo
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||
<span class="hidden lg:inline">Redo</span>
|
||||
</button>
|
||||
<button id="reset-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="refresh-ccw" class="w-4 h-4"></i> Reset
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<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-6 mx-2"></div>
|
||||
<span class="text-gray-400 text-sm">Selection:</span>
|
||||
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Selection:</span>
|
||||
<button id="select-all-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="check-square" class="w-4 h-4"></i> Select All
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<i data-lucide="check-square" class="w-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="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="square" class="w-4 h-4"></i> Deselect All
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<i data-lucide="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-6 mx-2"></div>
|
||||
<span class="text-gray-400 text-sm">Bulk Actions:</span>
|
||||
<div class="border-l border-gray-600 h-6 mx-1"></div>
|
||||
<span class="text-gray-400 text-xs sm:text-sm hidden md:inline">Bulk:</span>
|
||||
<button id="bulk-rotate-left-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="rotate-ccw" class="w-4 h-4"></i> Rotate Left
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<i data-lucide="rotate-ccw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||
<span class="hidden xl:inline">Rotate Left</span>
|
||||
</button>
|
||||
<button id="bulk-rotate-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="rotate-cw" class="w-4 h-4"></i> Rotate Right
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<i data-lucide="rotate-cw" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||
<span class="hidden xl:inline">Rotate Right</span>
|
||||
</button>
|
||||
<button id="bulk-delete-btn"
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i> Delete
|
||||
class="btn bg-red-600 hover:bg-red-700 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<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>
|
||||
<button id="bulk-duplicate-btn"
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="copy" class="w-4 h-4"></i> Duplicate
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<i data-lucide="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="btn bg-gray-700 hover:bg-gray-600 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="scissors" class="w-4 h-4"></i> Split
|
||||
class="btn bg-gray-700 hover:bg-gray-600 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<i data-lucide="scissors" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||
<span class="hidden xl:inline">Split</span>
|
||||
</button>
|
||||
<button id="bulk-download-btn"
|
||||
class="btn bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded text-sm flex items-center gap-2">
|
||||
<i data-lucide="download" class="w-4 h-4"></i> Download Selected
|
||||
class="btn bg-green-600 hover:bg-green-700 text-white px-2 sm:px-3 py-1.5 sm:py-2 rounded text-xs sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||
<span class="hidden xl:inline">Download Selected</span>
|
||||
</button>
|
||||
<div class="flex-1"></div>
|
||||
<div class="flex-1 min-w-[20px]"></div>
|
||||
<button id="export-pdf-btn"
|
||||
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded flex items-center gap-2">
|
||||
<i data-lucide="download" class="w-4 h-4"></i> Export PDF
|
||||
class="btn bg-indigo-600 hover:bg-indigo-700 text-white px-2 sm:px-4 py-1.5 sm:py-2 rounded flex items-center gap-1 sm:gap-2 text-sm">
|
||||
<i data-lucide="download" class="w-3 h-3 sm:w-4 sm:h-4"></i>
|
||||
<span class="hidden sm:inline">Export PDF</span>
|
||||
<span class="sm:hidden">Export</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Area -->
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
|
||||
Reference in New Issue
Block a user