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:
abdullahalam123
2025-11-10 21:54:41 +05:30
parent 0634600073
commit 7e14c83ab8
11 changed files with 1161 additions and 1321 deletions

View File

@@ -105,7 +105,7 @@ You can run BentoPDF locally for development or personal use.
### 🚀 Quick Start with Docker
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/LWO8I0?referralCode=LokiSalmonNeko)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/K4AU2B)
You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository:

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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.',
},
{

View File

@@ -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();
// }
// }

View File

@@ -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();
// }
// }

View File

@@ -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,
};

View File

@@ -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');

View File

@@ -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(() => {

View File

@@ -187,3 +187,10 @@ export function initializeIcons(): void {
},
});
}
export function formatStars(num: number) {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toLocaleString();
};

View File

@@ -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">