Compare commits

..
Author SHA1 Message Date
dependabot[bot] f223d1edbe build(deps): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-11-17 21:34:34 +00:00
82 changed files with 2604 additions and 9262 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v6
with: with:
node-version: "20" node-version: "20"
+7 -13
View File
@@ -1,6 +1,8 @@
<p align="center"><img src="public/images/favicon.svg" width="80"></p> <p align="center"><img src="public/images/favicon.svg" width="80"></p>
<h1 align="center">BentoPDF</h1> <h1 align="center">BentoPDF</h1>
<hr>
**BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that is self hostable and allows you to manipulate, edit, merge, and process PDF files directly in your browser. No server-side processing is required, ensuring your files remain secure and private. **BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that is self hostable and allows you to manipulate, edit, merge, and process PDF files directly in your browser. No server-side processing is required, ensuring your files remain secure and private.
![Docker Pulls](https://img.shields.io/docker/pulls/bentopdf/bentopdf) [![Ko-fi](https://img.shields.io/badge/Buy%20me%20a%20Coffee-yellow?logo=kofi&style=flat-square)](https://ko-fi.com/alio0) ![GitHub Stars](https://img.shields.io/github/stars/alam00000/bentopdf?style=social) ![Docker Pulls](https://img.shields.io/docker/pulls/bentopdf/bentopdf) [![Ko-fi](https://img.shields.io/badge/Buy%20me%20a%20Coffee-yellow?logo=kofi&style=flat-square)](https://ko-fi.com/alio0) ![GitHub Stars](https://img.shields.io/github/stars/alam00000/bentopdf?style=social)
@@ -8,17 +10,6 @@
![BentoPDF Tools](public/images/bentopdf-tools.png) ![BentoPDF Tools](public/images/bentopdf-tools.png)
---
## 📜 Licensing
BentoPDF is dual-licensed:
- **AGPL-3.0** for open-source projects where you share your full source code publicly
- **Commercial License** for proprietary/closed-source applications - **[Get Lifetime License for $49](https://ko-fi.com/s/f32ca4cb75)** (a one-time lifetime purchase, includes all feature updates forever)
For more details, see our [Licensing Page](https://bentopdf.com/licensing.html)
<hr> <hr>
## ⭐ Stargazers over time ## ⭐ Stargazers over time
@@ -71,7 +62,6 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
| Tool Name | Description | | Tool Name | Description |
| :--------------------- | :---------------------------------------------------------- | | :--------------------- | :---------------------------------------------------------- |
| **PDF Editor** | A comprehensive editor to modify your PDFs. | | **PDF Editor** | A comprehensive editor to modify your PDFs. |
| **Create Fillable Forms** | Create professional fillable PDF forms with text fields, checkboxes, dropdowns, radio buttons, signatures, and more. Fully compliant with PDF standards for compatibility with all PDF viewers. |
| **Add Page Numbers** | Easily add page numbers with customizable formatting. | | **Add Page Numbers** | Easily add page numbers with customizable formatting. |
| **Add Watermark** | Add text or image watermarks to protect your documents. | | **Add Watermark** | Add text or image watermarks to protect your documents. |
| **Header & Footer** | Add customizable headers and footers. | | **Header & Footer** | Add customizable headers and footers. |
@@ -190,7 +180,7 @@ The easiest way to self-host is to download the pre-built distribution file from
**Navigate to the extracted Folder:** **Navigate to the extracted Folder:**
```bash ```bash
# Navigate to the extracted folder # Navigate to the extracted folder
cd dist-1.7.3 # Replace with your version cd dist-1.6.2 # Replace with your version
``` ```
**Serve with Python:** **Serve with Python:**
@@ -445,3 +435,7 @@ BentoPDF wouldn't be possible without the amazing open-source tools and librarie
- **[cpdf](https://www.coherentpdf.com/)** – For content preserving pdf operations. - **[cpdf](https://www.coherentpdf.com/)** – For content preserving pdf operations.
Your work inspires and empowers developers everywhere. Thank you for making open-source amazing! Your work inspires and empowers developers everywhere. Thank you for making open-source amazing!
## 📜 License
This project is licensed under the **GNU AGPLv3**. See the [LICENSE](https://github.com/alam00000/bentopdf/blob/main/LICENSE) file for details.
+95 -34
View File
@@ -1,6 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -12,8 +11,15 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> 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"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
@@ -24,25 +30,52 @@
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a> <a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center"> <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" 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> <span class="sr-only">Open main menu</span>
<!-- Hamburger Icon --> <!-- 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" <svg
stroke="currentColor" aria-hidden="true"> id="menu-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> 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> </svg>
<!-- Close Icon --> <!-- Close Icon -->
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" <svg
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> id="close-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> 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> </svg>
</button> </button>
</div> </div>
@@ -50,13 +83,17 @@
</div> </div>
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a> <a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="./licensing.html" class="mobile-nav-link">Licensing</a> <a href="index.html#tools-header" class="mobile-nav-link"
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a> >All Tools</a
>
</div> </div>
</div> </div>
</nav> </nav>
@@ -74,7 +111,10 @@
<section id="mission-section" class="py-16 max-w-4xl mx-auto"> <section id="mission-section" class="py-16 max-w-4xl mx-auto">
<div class="text-center"> <div class="text-center">
<i data-lucide="rocket" class="w-16 h-16 text-indigo-400 mx-auto mb-6"></i> <i
data-lucide="rocket"
class="w-16 h-16 text-indigo-400 mx-auto mb-6"
></i>
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4"> <h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Our Mission Our Mission
</h2> </h2>
@@ -87,10 +127,14 @@
</div> </div>
</section> </section>
<div class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700"> <div
class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center"> <div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
<div class="text-center md:text-left"> <div class="text-center md:text-left">
<span class="text-indigo-400 font-bold uppercase">Our Core Philosophy</span> <span class="text-indigo-400 font-bold uppercase"
>Our Core Philosophy</span
>
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4"> <h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">
Privacy First. Always. Privacy First. Always.
</h2> </h2>
@@ -105,9 +149,16 @@
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<div class="relative w-48 h-48"> <div class="relative w-48 h-48">
<div class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"></div> <div
<div class="absolute inset-4 bg-indigo-500 rounded-full opacity-30 animate-pulse delay-500"></div> class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"
<i data-lucide="shield-check" class="w-48 h-48 text-indigo-400"></i> ></div>
<div
class="absolute inset-4 bg-indigo-500 rounded-full opacity-30 animate-pulse delay-500"
></div>
<i
data-lucide="shield-check"
class="w-48 h-48 text-indigo-400"
></i>
</div> </div>
</div> </div>
</div> </div>
@@ -119,7 +170,10 @@
</h2> </h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto"> <div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4"> <div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i> <i
data-lucide="zap"
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
></i>
<div> <div>
<h3 class="text-xl font-bold text-white">Built for Speed</h3> <h3 class="text-xl font-bold text-white">Built for Speed</h3>
<p class="text-gray-400 mt-2"> <p class="text-gray-400 mt-2">
@@ -130,7 +184,10 @@
</div> </div>
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4"> <div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
<i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i> <i
data-lucide="badge-dollar-sign"
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
></i>
<div> <div>
<h3 class="text-xl font-bold text-white">Completely Free</h3> <h3 class="text-xl font-bold text-white">Completely Free</h3>
<p class="text-gray-400 mt-2"> <p class="text-gray-400 mt-2">
@@ -141,7 +198,10 @@
</div> </div>
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4"> <div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
<i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i> <i
data-lucide="user-plus"
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
></i>
<div> <div>
<h3 class="text-xl font-bold text-white">No Account Required</h3> <h3 class="text-xl font-bold text-white">No Account Required</h3>
<p class="text-gray-400 mt-2"> <p class="text-gray-400 mt-2">
@@ -152,7 +212,10 @@
</div> </div>
</div> </div>
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4"> <div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
<i data-lucide="code-2" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i> <i
data-lucide="code-2"
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
></i>
<div> <div>
<h3 class="text-xl font-bold text-white">Open Source Spirit</h3> <h3 class="text-xl font-bold text-white">Open Source Spirit</h3>
<p class="text-gray-400 mt-2"> <p class="text-gray-400 mt-2">
@@ -177,8 +240,10 @@
needs. Experience the difference that privacy and performance can needs. Experience the difference that privacy and performance can
make. make.
</p> </p>
<a href="index.html#tools-header" <a
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"> href="index.html#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"
>
Explore All Tools Explore All Tools
</a> </a>
</section> </section>
@@ -196,7 +261,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -218,9 +283,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -241,7 +303,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -269,8 +331,7 @@
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+60 -24
View File
@@ -1,6 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -12,8 +11,15 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> 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"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
@@ -24,25 +30,52 @@
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a> <a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center"> <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" 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> <span class="sr-only">Open main menu</span>
<!-- Hamburger Icon --> <!-- 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" <svg
stroke="currentColor" aria-hidden="true"> id="menu-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> 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> </svg>
<!-- Close Icon --> <!-- Close Icon -->
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" <svg
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> id="close-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> 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> </svg>
</button> </button>
</div> </div>
@@ -50,13 +83,17 @@
</div> </div>
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a> <a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="./licensing.html" class="mobile-nav-link">Licensing</a> <a href="index.html#tools-header" class="mobile-nav-link"
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a> >All Tools</a
>
</div> </div>
</div> </div>
</nav> </nav>
@@ -75,8 +112,11 @@
<div class="max-w-2xl mx-auto text-center py-8"> <div class="max-w-2xl mx-auto text-center py-8">
<p class="text-lg text-gray-400"> <p class="text-lg text-gray-400">
You can reach us directly by email at: You can reach us directly by email at:
<a href="mailto:[email protected]" <a
class="text-indigo-400 underline hover:text-indigo-300">[email protected]</a> href="mailto:[email protected]"
class="text-indigo-400 underline hover:text-indigo-300"
>[email protected]</a
>
</p> </p>
</div> </div>
</div> </div>
@@ -93,7 +133,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -115,9 +155,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -138,7 +175,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -166,8 +203,7 @@
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+115 -40
View File
@@ -1,6 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -12,8 +11,15 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> 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"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
@@ -24,25 +30,52 @@
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a> <a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center"> <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" 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> <span class="sr-only">Open main menu</span>
<!-- Hamburger Icon --> <!-- 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" <svg
stroke="currentColor" aria-hidden="true"> id="menu-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> 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> </svg>
<!-- Close Icon --> <!-- Close Icon -->
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" <svg
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> id="close-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> 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> </svg>
</button> </button>
</div> </div>
@@ -50,13 +83,17 @@
</div> </div>
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a> <a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="./licensing.html" class="mobile-nav-link">Licensing</a> <a href="index.html#tools-header" class="mobile-nav-link"
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a> >All Tools</a
>
</div> </div>
</div> </div>
</nav> </nav>
@@ -73,12 +110,17 @@
</section> </section>
<div class="max-w-4xl mx-auto space-y-4"> <div class="max-w-4xl mx-auto space-y-4">
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"> <details
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
>
<summary class="flex items-center justify-between cursor-pointer"> <summary class="flex items-center justify-between cursor-pointer">
<h3 class="font-semibold text-white text-lg"> <h3 class="font-semibold text-white text-lg">
Are my files safe and private? Are my files safe and private?
</h3> </h3>
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i> <i
data-lucide="plus"
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
></i>
</summary> </summary>
<div class="mt-4 text-gray-400"> <div class="mt-4 text-gray-400">
<p> <p>
@@ -90,12 +132,17 @@
</div> </div>
</details> </details>
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"> <details
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
>
<summary class="flex items-center justify-between cursor-pointer"> <summary class="flex items-center justify-between cursor-pointer">
<h3 class="font-semibold text-white text-lg"> <h3 class="font-semibold text-white text-lg">
Is BentoPDF really free? What's the catch? Is BentoPDF really free? What's the catch?
</h3> </h3>
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i> <i
data-lucide="plus"
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
></i>
</summary> </summary>
<div class="mt-4 text-gray-400"> <div class="mt-4 text-gray-400">
<p> <p>
@@ -107,12 +154,17 @@
</div> </div>
</details> </details>
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"> <details
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
>
<summary class="flex items-center justify-between cursor-pointer"> <summary class="flex items-center justify-between cursor-pointer">
<h3 class="font-semibold text-white text-lg"> <h3 class="font-semibold text-white text-lg">
Do I need an internet connection to use the tools? Do I need an internet connection to use the tools?
</h3> </h3>
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i> <i
data-lucide="plus"
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
></i>
</summary> </summary>
<div class="mt-4 text-gray-400"> <div class="mt-4 text-gray-400">
<p> <p>
@@ -126,12 +178,17 @@
</div> </div>
</details> </details>
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"> <details
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
>
<summary class="flex items-center justify-between cursor-pointer"> <summary class="flex items-center justify-between cursor-pointer">
<h3 class="font-semibold text-white text-lg"> <h3 class="font-semibold text-white text-lg">
Are there any file size or usage limitations? Are there any file size or usage limitations?
</h3> </h3>
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i> <i
data-lucide="plus"
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
></i>
</summary> </summary>
<div class="mt-4 text-gray-400"> <div class="mt-4 text-gray-400">
<p> <p>
@@ -144,12 +201,17 @@
</div> </div>
</details> </details>
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"> <details
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
>
<summary class="flex items-center justify-between cursor-pointer"> <summary class="flex items-center justify-between cursor-pointer">
<h3 class="font-semibold text-white text-lg"> <h3 class="font-semibold text-white text-lg">
Why did my PDF fail to process? Why did my PDF fail to process?
</h3> </h3>
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i> <i
data-lucide="plus"
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
></i>
</summary> </summary>
<div class="mt-4 text-gray-400"> <div class="mt-4 text-gray-400">
<p>Failures are rare but can happen for a few reasons:</p> <p>Failures are rare but can happen for a few reasons:</p>
@@ -170,19 +232,29 @@
</div> </div>
</details> </details>
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"> <details
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
>
<summary class="flex items-center justify-between cursor-pointer"> <summary class="flex items-center justify-between cursor-pointer">
<h3 class="font-semibold text-white text-lg"> <h3 class="font-semibold text-white text-lg">
Do you track my activity on BentoPDF? Do you track my activity on BentoPDF?
</h3> </h3>
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i> <i
data-lucide="plus"
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
></i>
</summary> </summary>
<div class="mt-4 text-gray-400"> <div class="mt-4 text-gray-400">
<p> <p>
We care about your privacy. BentoPDF does not track personal We care about your privacy. BentoPDF does not track personal
information. We use information. We use
<a href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank" <a
rel="noopener noreferrer">Simple Analytics</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 solely to see anonymous visit counts. This means we can know how
many users visit our site, but many users visit our site, but
<strong>we never know who you are</strong>. Simple Analytics is <strong>we never know who you are</strong>. Simple Analytics is
@@ -191,12 +263,17 @@
</div> </div>
</details> </details>
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"> <details
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
>
<summary class="flex items-center justify-between cursor-pointer"> <summary class="flex items-center justify-between cursor-pointer">
<h3 class="font-semibold text-white text-lg"> <h3 class="font-semibold text-white text-lg">
What technology does BentoPDF use? What technology does BentoPDF use?
</h3> </h3>
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i> <i
data-lucide="plus"
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
></i>
</summary> </summary>
<div class="mt-4 text-gray-400"> <div class="mt-4 text-gray-400">
<p> <p>
@@ -221,8 +298,10 @@
If you can't find the answer you're looking for, feel free to reach If you can't find the answer you're looking for, feel free to reach
out to our support team. We're always Bento to help. out to our support team. We're always Bento to help.
</p> </p>
<a href="contact.html" <a
class="inline-block px-8 py-3 rounded-full bg-gray-700 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:bg-gray-600 transition-colors duration-200"> href="contact.html"
class="inline-block px-8 py-3 rounded-full bg-gray-700 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:bg-gray-600 transition-colors duration-200"
>
Contact Us Contact Us
</a> </a>
</section> </section>
@@ -240,7 +319,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -262,9 +341,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -285,7 +361,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -313,8 +389,7 @@
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+30 -259
View File
@@ -25,40 +25,11 @@
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a> <a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
<a href="https://github.com/alam00000/bentopdf/" target="_blank" rel="noopener noreferrer" class="
inline-flex items-center gap-1.5 text-sm font-medium
bg-gray-800 text-gray-200 border border-gray-600
pl-2.5 pr-3 py-1
rounded-full transition-colors duration-200
shadow-sm hover:shadow-md hover:bg-gray-700
">
<svg class="w-4 h-4 flex-shrink-0 text-gray-200" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
<span id="github-stars-desktop">-</span>
</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center gap-4"> <div class="md:hidden flex items-center">
<a href="https://github.com/alam00000/bentopdf/" target="_blank" rel="noopener noreferrer" class="
inline-flex items-center gap-1.5 text-sm font-medium
bg-gray-800 text-gray-200 border border-gray-600
pl-2.5 pr-3 py-1
rounded-full transition-colors duration-200
shadow-sm hover:shadow-md hover:bg-gray-700
">
<svg class="w-4 h-4 flex-shrink-0 text-gray-200" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
<span id="github-stars-mobile">-</span>
</a>
<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" 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">
@@ -84,112 +55,57 @@
<a href="index.html" class="mobile-nav-link">Home</a> <a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="./licensing.html" class="mobile-nav-link">Licensing</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>
</div> </div>
</nav> </nav>
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8"> <div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
<section id="hero-section" class="text-center py-20 relative"> <section id="hero-section" class="text-center py-20">
<div class="bg-grid-pattern"></div>
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4"> <h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
The <span The <span class="marker-slanted"> PDF Toolkit </span> built for
class="inline-flex items-center gap-3 border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 align-middle">
PDF Toolkit <i data-lucide="briefcase" class="w-8 h-8 md:w-12 md:h-12 transform -rotate-4"></i> </span> built
for
privacy<span privacy<span
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span> class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
</h1> </h1>
<!-- <p class="text-lg text-gray-400 mb-12">Fast, Secure and Forever Free.</p> --> <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-12 mt-12"> <div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8">
<span <span
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium"> 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 text-gray-400"></i> <i data-lucide="check-circle" class="w-4 h-4"></i>
No Signups No Signups
</span> </span>
<span <span
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium"> 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 text-gray-400"></i> <i data-lucide="check-circle" class="w-4 h-4"></i>
Unlimited Use Unlimited Use
</span> </span>
<span <span
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium"> 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 text-gray-400"></i> <i data-lucide="check-circle" class="w-4 h-4"></i>
Works Offline Works Offline
</span> </span>
</div> </div>
<div class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-4">
<a href="#tools-header" <a href="#tools-header"
class="inline-flex items-center gap-3 pl-8 pr-2 py-2 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"> 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
Start Using Now Using - Forever Free</a>
<span class="bg-white text-indigo-600 rounded-full p-2"> <a href="https://github.com/alam00000/bentopdf/stargazers" target="_blank" rel="noopener noreferrer" class="
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" inline-flex items-center gap-1.5 text-sm font-medium
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"> bg-white text-gray-800 border border-gray-300
<path d="M5 12h14" /> dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600
<path d="m12 5 7 7-7 7" /> pl-2.5 pr-3 py-1
</svg> rounded-full transition-colors duration-200
</span> shadow-sm hover:shadow-md hover:bg-gray-50 dark:hover:bg-gray-700
</a> ">
<svg class="w-4 h-4 flex-shrink-0 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 24 24"
</div> aria-hidden="true">
</section>
<!-- USED BY SECTION START -->
<section class="py-10 hide-section">
<div class="container mx-auto px-4">
<p class="text-center text-gray-500 text-sm font-medium mb-8">Used by companies and people working at</p>
<div
class="flex flex-wrap justify-center items-center gap-8 md:gap-12 opacity-70 grayscale hover:grayscale-0 transition-all duration-500">
<!-- Google -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 24 24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .533 5.347.533 12S5.867 24 12.48 24c3.44 0 6.013-1.133 8.053-3.227 2.107-2.08 2.72-5.027 2.72-7.44 0-.733-.053-1.44-.16-2.133h-10.613z" />
</svg>
<!-- Microsoft -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 24 24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 0h11.377v11.372H0zM12.623 0H24v11.372H12.623zM0 12.623h11.377V24H0zM12.623 12.623H24V24H12.623z" />
</svg>
<!-- Reddit -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 24 24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z" />
</svg>
<!-- Amazon -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 48 48"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M25.4026553,25.9595294 C24.660417,27.4418824 23.3876054,28.3962353 22.0103725,28.7181176 C21.8015298,28.7181176 21.4826213,28.8225882 21.1637129,28.8225882 C18.835399,28.8225882 17.458166,27.0211765 17.458166,24.3727059 C17.458166,20.9788235 19.4703937,19.392 22.0103725,18.6465882 C23.3876054,18.3303529 24.9793255,18.2230588 26.5682233,18.2230588 L26.5682233,19.4964706 C26.5682233,21.9331765 26.6726447,23.8390588 25.4026553,25.9595294 L25.4026553,25.9595294 Z M26.5682233,13.3524706 C25.1909904,13.4569412 23.5992703,13.5614118 22.0103725,13.7703529 C19.574815,14.0922353 17.1392576,14.5157647 15.1298521,15.4701176 C11.2098182,17.0597647 8.55977364,20.4508235 8.55977364,25.4287059 C8.55977364,31.6856471 12.5842289,34.8621176 17.6726531,34.8621176 C19.3659723,34.8621176 20.7432053,34.6475294 22.0103725,34.3341176 C24.0282445,33.696 25.7187415,32.5298824 27.7309692,30.4094118 C28.8965372,31.9990588 29.2182679,32.7444706 31.2276733,34.4385882 C31.7582467,34.6475294 32.28882,34.6475294 32.7093276,34.3341176 C33.9821392,33.2724706 36.208854,31.3637647 37.3715998,30.3049412 C37.9021732,29.8814118 37.7977518,29.2432941 37.4760212,28.7181176 C36.3132753,27.2329412 35.1448851,25.9595294 35.1448851,23.0992941 L35.1448851,13.5614118 C35.1448851,9.53505882 35.4666157,5.82494118 32.5004849,3.072 C30.0649275,0.849882353 26.2493149,0 23.2831841,0 L22.0103725,0 C16.6115064,0.313411765 10.8937319,2.64564706 9.61809814,9.32329412 C9.40643324,10.1731765 10.0442501,10.4894118 10.4675799,10.5938824 L16.3998415,11.3364706 C17.0348362,11.2291765 17.3537447,10.6983529 17.458166,10.1731765 C17.9859172,7.84094118 19.8937235,6.67482353 22.0103725,6.46023529 L22.4365245,6.46023529 C23.7093361,6.46023529 25.086569,6.99105882 25.8259851,8.05270588 C26.6726447,9.32329412 26.5682233,11.0202353 26.5682233,12.5054118 L26.5682233,13.3524706 L26.5682233,13.3524706 Z">
</path>
<path
d="M47.9943556,35.9463529 L47.9943556,35.9435294 C47.971778,35.4437647 47.8673567,35.0625882 47.658514,34.7463529 L47.6359364,34.7152941 L47.6105366,34.6842353 C47.3988717,34.4527059 47.1956734,34.3651765 46.9755419,34.2691765 C46.3179696,34.0150588 45.3612442,33.8795294 44.2097872,33.8767059 C43.382883,33.8767059 42.4713128,33.9557647 41.5540982,34.1562353 L41.551276,34.0941176 L40.6284171,34.4018824 L40.6114839,34.4103529 L40.0893771,34.5797647 L40.0893771,34.6023529 C39.47696,34.8564706 38.9209869,35.1727059 38.4045245,35.5482353 C38.0827939,35.7882353 37.8175072,36.1072941 37.8033962,36.5957647 C37.7949296,36.8611765 37.9303952,37.1661176 38.1533489,37.3468235 C38.3763025,37.5275294 38.6359448,37.5896471 38.8645429,37.5896471 C38.9181647,37.5896471 38.9689643,37.5868235 39.0141194,37.5783529 L39.0592746,37.5755294 L39.093141,37.5698824 C39.5446928,37.4738824 40.2022651,37.4089412 40.9727253,37.3016471 C41.6331198,37.2282353 42.3330251,37.1745882 42.9397978,37.1745882 C43.368772,37.1717647 43.7554132,37.2028235 44.0206999,37.2592941 C44.1533432,37.2875294 44.2521202,37.3214118 44.3057419,37.3496471 C44.3254973,37.3552941 44.3396083,37.3637647 44.3480749,37.3694118 C44.3593637,37.4061176 44.3762969,37.5021176 44.3734747,37.6348235 C44.3791191,38.1430588 44.164632,39.0861176 43.8683012,40.0065882 C43.5804369,40.9270588 43.2304843,41.8503529 42.999064,42.4630588 C42.94262,42.6042353 42.9059314,42.7595294 42.9059314,42.9289412 C42.900287,43.1745882 43.0018862,43.4738824 43.2163733,43.6715294 C43.425216,43.8691765 43.696147,43.9482353 43.9219229,43.9482353 L43.9332117,43.9482353 C44.2718756,43.9454118 44.5597398,43.8098824 44.8080933,43.6150588 C47.1505182,41.5087059 47.9661336,38.1430588 48,36.2484706 L47.9943556,35.9463529 Z M41.0489247,38.8658824 C40.8090378,38.8630588 40.5635065,38.9195294 40.3349084,39.0268235 C40.0780883,39.1284706 39.8156239,39.2470588 39.5672704,39.3515294 L39.2032068,39.504 L38.7290774,39.6931765 L38.7290774,39.6988235 C33.5785648,41.7882353 28.16841,43.0136471 23.1618295,43.1209412 C22.9783866,43.1265882 22.7921215,43.1265882 22.614323,43.1265882 C14.7403887,43.1322353 8.31706456,39.4785882 1.83729642,35.8785882 C1.61152053,35.76 1.37727804,35.6978824 1.15150215,35.6978824 C0.860815683,35.6978824 0.561662624,35.808 0.344353327,36.0112941 C0.12704403,36.2174118 -0.00277710907,36.5138824 4.50895989e-05,36.816 C-0.00277710907,37.2084706 0.208887791,37.5698824 0.505218651,37.8042353 C6.58705678,43.0870588 13.25309,47.9943529 22.2192152,48 C22.3941915,48 22.57199,47.9943529 22.7497885,47.9915294 C28.453452,47.8644706 34.902176,45.936 39.9087564,42.7905882 L39.9398006,42.7708235 C40.5945507,42.3783529 41.2493008,41.9322353 41.8673623,41.4381176 C42.2511813,41.1529412 42.516468,40.7068235 42.516468,40.2437647 C42.4995348,39.4221176 41.8024517,38.8658824 41.0489247,38.8658824 Z">
</path>
</svg>
<!-- Oracle -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 16 16"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M.1 8c0 2.761 2.237 5 4.997 5h5.806A4.999 4.999 0 0015.9 8c0-2.761-2.237-5-4.997-5H5.097A4.999 4.999 0 00.1 8zm13.911 0a3.235 3.235 0 01-3.234 3.237h-5.55A3.235 3.235 0 011.991 8a3.235 3.235 0 013.234-3.236h5.551A3.235 3.235 0 0114.011 8z" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
<!-- Apple --> <span id="github-stars">-</span>
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors -ml-3" viewBox="0 0 24 24" </a>
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.78 1.18-.19 2.31-.89 3.51-.84 1.54.06 2.77.74 3.69 1.74-3.54 1.84-2.86 6.53.51 7.91-.48 1.13-1.1 2.25-2.79 3.38zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
</svg>
</div>
</div> </div>
</section> </section>
@@ -273,14 +189,8 @@
<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" class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Search for a tool (e.g., 'split', 'organize'...)" /> placeholder="Search for a tool (e.g., 'split', 'organize'...)" />
<span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2 gap-2"> <span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2">
<kbd id="shortcut" <kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd>
class="bg-gray-800 px-2.5 py-1.5 rounded-md text-xs font-semibold text-gray-300 border border-gray-700 shadow-sm"></kbd>
<button id="open-shortcuts-btn"
class="p-1 hover:bg-gray-600 rounded-md text-gray-400 hover:text-white transition-colors hidden-on-touch"
title="Keyboard Shortcuts">
<i data-lucide="settings" class="w-4 h-4"></i>
</button>
</span> </span>
</div> </div>
</div> </div>
@@ -342,134 +252,6 @@
</div> </div>
</div> </div>
<!-- Shortcuts Modal -->
<div id="shortcuts-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 w-full max-w-2xl rounded-xl border border-gray-700 shadow-2xl flex flex-col max-h-[80vh]">
<div class="p-6 border-b border-gray-700 flex justify-between items-center">
<h3 class="text-xl font-bold text-white flex items-center gap-2">
<i data-lucide="settings" class="w-6 h-6 text-indigo-400"></i>
Settings
</h3>
<button id="close-shortcuts-modal" class="text-gray-400 hover:text-white transition-colors">
<i data-lucide="x" class="w-6 h-6"></i>
</button>
</div>
<!-- Tabs -->
<div class="flex border-b border-gray-700">
<button id="shortcuts-tab-btn" class="flex-1 py-3 text-sm font-medium bg-indigo-600 text-white">
<i data-lucide="keyboard" class="inline-block w-4 h-4 mr-1"></i>
Shortcuts
</button>
<button id="preferences-tab-btn" class="flex-1 py-3 text-sm font-medium text-gray-300 hover:text-white">
<i data-lucide="sliders" class="inline-block w-4 h-4 mr-1"></i>
Preferences
</button>
</div>
<!-- Shortcuts Tab Content -->
<div id="shortcuts-tab-content" class="p-6 overflow-y-auto flex-grow">
<div class="flex justify-between items-center mb-6">
<div class="relative flex-grow mr-4">
<i data-lucide="search"
class="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-500"></i>
<input type="text" id="shortcut-search" placeholder="Search shortcuts..."
class="w-full bg-gray-900 border border-gray-700 text-white rounded-lg pl-10 pr-4 py-2 focus:ring-indigo-500 focus:border-indigo-500">
</div>
</div>
<div class="mb-4 p-3 bg-indigo-900/20 border border-indigo-500/30 rounded-lg flex items-start gap-3">
<i data-lucide="info" class="w-5 h-5 text-indigo-400 flex-shrink-0 mt-0.5"></i>
<p class="text-sm text-indigo-200">
Press and hold keys to set a shortcut. Changes are <strong>auto-saved</strong>.
<br><span class="text-yellow-300">⚠️ Avoid common browser shortcuts (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N
etc.) as they
may not work reliably.</span>
</p>
</div>
<div id="shortcuts-list" class="space-y-2">
<!-- Shortcuts will be populated here -->
</div>
</div>
<!-- Preferences Tab Content -->
<div id="preferences-tab-content" class="hidden p-6 overflow-y-auto flex-grow">
<h4 class="text-lg font-semibold text-white mb-4">Display Preferences</h4>
<div class="space-y-4">
<div class="flex items-center justify-between p-4 bg-gray-900 rounded-lg border border-gray-700">
<div class="flex-1">
<label for="full-width-toggle" class="text-sm font-medium text-gray-200 cursor-pointer">
Full Width Mode
</label>
<p class="text-xs text-gray-400 mt-1">
Use the full screen width for all tools instead of a centered container
</p>
</div>
<label class="relative inline-flex items-center cursor-pointer ml-4">
<input type="checkbox" id="full-width-toggle" class="sr-only peer">
<div
class="w-11 h-6 bg-gray-700 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-indigo-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600">
</div>
</label>
</div>
</div>
</div>
<div class="p-6 border-t border-gray-700 flex justify-between items-center bg-gray-850 rounded-b-xl">
<div id="shortcuts-tab-footer" class="flex gap-3">
<button id="import-shortcuts-btn"
class="text-gray-400 hover:text-white text-sm font-medium flex items-center gap-1 transition-colors">
<i data-lucide="upload" class="w-4 h-4"></i> Import
</button>
<button id="export-shortcuts-btn"
class="text-gray-400 hover:text-white text-sm font-medium flex items-center gap-1 transition-colors">
<i data-lucide="download" class="w-4 h-4"></i> Export
</button>
</div>
<div id="preferences-tab-footer" class="hidden w-full">
<p class="text-xs text-gray-400 text-center">Settings are automatically saved</p>
</div>
<button id="reset-shortcuts-btn"
class="text-red-400 hover:text-red-300 text-sm font-medium transition-colors">
Reset to Defaults
</button>
</div>
</div>
</div>
<!-- Warning Modal -->
<div id="warning-modal"
class="fixed inset-0 bg-black/70 backdrop-blur-sm z-50 hidden items-center justify-center p-4">
<div
class="bg-gray-800 rounded-xl border border-gray-700 shadow-2xl max-w-md w-full overflow-hidden animate-scale-in">
<div class="p-6">
<div class="flex items-start gap-4 mb-4">
<div id="warning-icon"
class="w-12 h-12 flex items-center justify-center rounded-full bg-yellow-500/10 flex-shrink-0">
<i data-lucide="alert-triangle" class="w-6 h-6 text-yellow-500"></i>
</div>
<div class="flex-1">
<h3 id="warning-title" class="text-xl font-bold text-white mb-2">Warning</h3>
<p id="warning-message" class="text-gray-300 text-sm leading-relaxed"></p>
</div>
</div>
</div>
<div class="flex gap-3 p-4 bg-gray-850 border-t border-gray-700">
<button id="warning-cancel-btn"
class="flex-1 px-4 py-2.5 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors">
Cancel
</button>
<button id="warning-confirm-btn"
class="flex-1 px-4 py-2.5 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg font-medium transition-colors">
Proceed
</button>
</div>
</div>
</div>
<div class="section-divider hide-section mb-20 mt-10"></div> <div class="section-divider hide-section mb-20 mt-10"></div>
<!-- COMPLIANCE SECTION START --> <!-- COMPLIANCE SECTION START -->
@@ -798,13 +580,6 @@
</section> </section>
<div id="signature-ghost" class="hidden"></div> <div id="signature-ghost" class="hidden"></div>
<!-- Scroll to Top Button -->
<button id="scroll-to-top-btn"
class="fixed bottom-12 right-8 bg-indigo-600 hover:bg-indigo-700 text-white p-3 rounded-full shadow-lg transition-all duration-300 opacity-0 translate-y-10 invisible z-40"
aria-label="Scroll to top">
<i data-lucide="arrow-up" class="w-6 h-6"></i>
</button>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8"> <footer class="mt-16 border-t-2 border-gray-700 py-8">
@@ -819,7 +594,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -841,9 +616,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -864,7 +636,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -892,7 +664,6 @@
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/main.ts"></script> <script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
-677
View File
@@ -1,677 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Licensing - BentoPDF</title>
<link href="/src/css/styles.css" rel="stylesheet" />
</head>
<body class="antialiased bg-gray-900 text-gray-300">
<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" />
<span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a>
</span>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
<!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center">
<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">
<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>
<!-- 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>
</button>
</div>
</div>
</div>
<!-- Mobile Menu Dropdown -->
<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="./licensing.html" class="mobile-nav-link">Licensing</a>
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
</div>
</div>
</nav>
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
<section id="licensing-hero" class="text-center py-16 md:py-24">
<h1 class="text-3xl md:text-6xl font-bold text-white mb-4">
Licensing for
<span class="marker-slanted">BentoPDF</span>
</h1>
<p class="text-lg md:text-xl text-gray-400">
Choose the license that fits your needs.
</p>
</section>
<div class="section-divider"></div>
<section id="licensing-options" class="py-16 max-w-6xl mx-auto">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Open Source License -->
<div class="bg-gray-800 rounded-xl p-8 border border-gray-700">
<div class="flex items-center gap-4 mb-6">
<i data-lucide="code-2" class="w-12 h-12 text-green-400 flex-shrink-0"></i>
<h2 class="text-3xl font-bold text-white">Open Source</h2>
</div>
<p class="text-gray-400 mb-6 leading-relaxed">
BentoPDF is licensed under the
<strong class="text-white">GNU Affero General Public License v3.0 (AGPL-3.0)</strong>.
You are free to use, modify, and distribute BentoPDF in your open-source projects.
</p>
<div class="space-y-4">
<div class="flex items-start gap-3">
<i data-lucide="check-circle" class="w-6 h-6 text-green-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-1">Free to Use</h3>
<p class="text-gray-400 text-sm">
Use BentoPDF for free in open-source projects where you
make your source code publicly available.
</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="check-circle" class="w-6 h-6 text-green-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-1">
Modify & Distribute
</h3>
<p class="text-gray-400 text-sm">
Modify the source code and distribute your version, as long
as you comply with AGPL-3.0 requirements.
</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="alert-circle" class="w-6 h-6 text-yellow-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-1">Share Alike</h3>
<p class="text-gray-400 text-sm">
Any modifications or derivative works must also be licensed
under AGPL-3.0 and made publicly available.
</p>
</div>
</div>
</div>
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
class="inline-block mt-8 px-6 py-3 rounded-lg bg-green-600 text-white font-semibold hover:bg-green-700 transition-colors">
View on GitHub
</a>
</div>
<!-- Commercial License -->
<div class="bg-gray-800 rounded-xl p-8 border border-indigo-500">
<div class="flex items-center gap-4 mb-6">
<i data-lucide="briefcase" class="w-12 h-12 text-indigo-400 flex-shrink-0"></i>
<h2 class="text-3xl font-bold text-white">Commercial</h2>
</div>
<p class="text-gray-400 mb-6 leading-relaxed">
If you want to use BentoPDF in a
<strong class="text-white">closed-source or proprietary application</strong>
without sharing your source code, you must purchase a commercial license.
</p>
<div class="space-y-4">
<div class="flex items-start gap-3">
<i data-lucide="lock" class="w-6 h-6 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-1">
Proprietary Use
</h3>
<p class="text-gray-400 text-sm">
Use BentoPDF in closed-source applications without the
obligation to disclose your source code.
</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="shield-check" class="w-6 h-6 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-1">
Enterprise Support
</h3>
<p class="text-gray-400 text-sm">
Get priority support, custom features, and assistance with
integration.
</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="zap" class="w-6 h-6 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-1">
Early Access to New Features
</h3>
<p class="text-gray-400 text-sm">
Commercial license holders get early access to new features and updates.
</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="file-check" class="w-6 h-6 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-1">
Flexible Terms
</h3>
<p class="text-gray-400 text-sm">
Licensing terms tailored to your business needs and project
requirements.
</p>
</div>
</div>
</div>
<div
class="mt-8 bg-gradient-to-r from-indigo-900/40 to-purple-900/40 rounded-lg p-6 border border-indigo-500/50">
<div class="text-center">
<div class="inline-block bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full mb-3 animate-pulse">
LIMITED TIME OFFER
</div>
<p class="text-white text-2xl font-bold mb-2">Lifetime License</p>
<div class="flex items-center justify-center gap-3 mb-3">
<span class="text-gray-400 text-2xl line-through">$99</span>
<span class="text-indigo-300 text-4xl font-bold">$49</span>
<span class="text-lg font-normal text-gray-400">one-time</span>
</div>
<p class="text-gray-300 text-sm mb-4">Includes all feature updates forever</p>
<!-- Unlimited Usage Info -->
<div class="bg-indigo-900/30 rounded-lg p-4 mb-4 border border-indigo-500/30">
<div class="flex items-start gap-3 text-left">
<i data-lucide="infinity" class="w-6 h-6 text-indigo-400 flex-shrink-0 mt-0.5"></i>
<div>
<h4 class="text-white font-semibold text-sm mb-2">Unlimited Usage</h4>
<p class="text-gray-300 text-xs leading-relaxed">
The BentoPDF commercial license permits use on an <strong class="text-white">unrestricted, unlimited number of devices, servers, and user machines</strong> within your organization. There are <strong class="text-white">no per-user, per-machine, or per-seat limitations</strong>. Once you purchase the license, all your internal users may use BentoPDF through your browser-based system without requiring additional licenses.
</p>
</div>
</div>
</div>
<a href="https://ko-fi.com/s/f32ca4cb75" target="_blank" rel="noopener noreferrer"
class="inline-block px-8 py-3 rounded-lg bg-indigo-600 text-white font-semibold hover:bg-indigo-700 transition-colors shadow-lg hover:shadow-indigo-500/50 transform hover:scale-105 transition-all duration-200">
Purchase Lifetime License
</a>
</div>
</div>
<p class="mt-4 text-sm text-gray-400 italic text-center">
💡 Custom requests and development are available for separate charges. <a href="./contact.html"
class="text-indigo-400 hover:underline">Contact us</a> for details.
</p>
</div>
</div>
</section>
<div class="section-divider"></div>
<section id="license-comparison" class="py-16 max-w-6xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-4">
Do I Need a <span class="marker-slanted">Commercial License?</span>
</h2>
<p class="text-center text-gray-400 mb-12 max-w-3xl mx-auto">
Use this quick reference chart to determine if your use case requires a commercial license.
</p>
<div class="overflow-x-auto">
<table class="w-full bg-gray-800 rounded-xl overflow-hidden border border-gray-700">
<thead>
<tr class="bg-gray-700">
<th class="px-6 py-4 text-left text-white font-semibold border-b border-gray-600">Use Case
</th>
<th class="px-6 py-4 text-center text-white font-semibold border-b border-gray-600 w-48">
License Required</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700">
<tr class="hover:bg-gray-750 transition-colors">
<td class="px-6 py-4 text-gray-300">Personal use (not hosting as a service)</td>
<td class="px-6 py-4 text-center">
<span
class="inline-flex items-center gap-2 px-3 py-1 bg-green-900/30 text-green-400 rounded-full text-sm font-semibold">
<i data-lucide="check" class="w-4 h-4"></i> AGPL (Free)
</span>
</td>
</tr>
<tr class="hover:bg-gray-750 transition-colors">
<td class="px-6 py-4 text-gray-300">Open-source project with publicly available source code
</td>
<td class="px-6 py-4 text-center">
<span
class="inline-flex items-center gap-2 px-3 py-1 bg-green-900/30 text-green-400 rounded-full text-sm font-semibold">
<i data-lucide="check" class="w-4 h-4"></i> AGPL (Free)
</span>
</td>
</tr>
<tr class="hover:bg-gray-750 transition-colors">
<td class="px-6 py-4 text-gray-300">Public website where you share your full modified source
code under AGPL</td>
<td class="px-6 py-4 text-center">
<span
class="inline-flex items-center gap-2 px-3 py-1 bg-green-900/30 text-green-400 rounded-full text-sm font-semibold">
<i data-lucide="check" class="w-4 h-4"></i> AGPL (Free)
</span>
</td>
</tr>
<tr class="hover:bg-gray-750 transition-colors">
<td class="px-6 py-4 text-gray-300">Educational/research project with publicly shared code
</td>
<td class="px-6 py-4 text-center">
<span
class="inline-flex items-center gap-2 px-3 py-1 bg-green-900/30 text-green-400 rounded-full text-sm font-semibold">
<i data-lucide="check" class="w-4 h-4"></i> AGPL (Free)
</span>
</td>
</tr>
<tr class="bg-red-900/10 hover:bg-red-900/20 transition-colors">
<td class="px-6 py-4 text-gray-300 font-medium">SaaS or web application (closed-source)</td>
<td class="px-6 py-4 text-center">
<span
class="inline-flex items-center gap-2 px-3 py-1 bg-red-900/30 text-red-400 rounded-full text-sm font-semibold">
<i data-lucide="shopping-cart" class="w-4 h-4"></i> Commercial
</span>
</td>
</tr>
<tr class="bg-red-900/10 hover:bg-red-900/20 transition-colors">
<td class="px-6 py-4 text-gray-300 font-medium">Internal company tool/dashboard (source not
shared with users)</td>
<td class="px-6 py-4 text-center">
<span
class="inline-flex items-center gap-2 px-3 py-1 bg-red-900/30 text-red-400 rounded-full text-sm font-semibold">
<i data-lucide="shopping-cart" class="w-4 h-4"></i> Commercial
</span>
</td>
</tr>
<tr class="bg-red-900/10 hover:bg-red-900/20 transition-colors">
<td class="px-6 py-4 text-gray-300 font-medium">Commercial product (keeping source code
private)</td>
<td class="px-6 py-4 text-center">
<span
class="inline-flex items-center gap-2 px-3 py-1 bg-red-900/30 text-red-400 rounded-full text-sm font-semibold">
<i data-lucide="shopping-cart" class="w-4 h-4"></i> Commercial
</span>
</td>
</tr>
<tr class="bg-red-900/10 hover:bg-red-900/20 transition-colors">
<td class="px-6 py-4 text-gray-300 font-medium">Network service where source code is not
accessible to users</td>
<td class="px-6 py-4 text-center">
<span
class="inline-flex items-center gap-2 px-3 py-1 bg-red-900/30 text-red-400 rounded-full text-sm font-semibold">
<i data-lucide="shopping-cart" class="w-4 h-4"></i> Commercial
</span>
</td>
</tr>
<tr class="bg-red-900/10 hover:bg-red-900/20 transition-colors">
<td class="px-6 py-4 text-gray-300 font-medium">Any app where you want to keep modifications
private</td>
<td class="px-6 py-4 text-center">
<span
class="inline-flex items-center gap-2 px-3 py-1 bg-red-900/30 text-red-400 rounded-full text-sm font-semibold">
<i data-lucide="shopping-cart" class="w-4 h-4"></i> Commercial
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-8 text-center">
<p class="text-gray-400 text-sm">
Still not sure? <a href="./contact.html"
class="text-indigo-400 hover:underline font-semibold">Contact us</a> to discuss your specific
use case.
</p>
</div>
</section>
<div class="section-divider"></div>
<section id="agpl-requirements" class="py-16 max-w-6xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
AGPL <span class="marker-slanted">Requirements</span>
</h2>
<div class="bg-gray-800 rounded-xl p-8 border border-gray-700 mb-8">
<p class="text-gray-300 mb-6 leading-relaxed">
When using BentoPDF under AGPL-3.0, you must comply with the following requirements:
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="flex items-start gap-3">
<i data-lucide="file-text" class="w-6 h-6 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-2">Producer Line & Copyright</h3>
<p class="text-gray-400 text-sm">
You should mention our open-source and include AGPL license details in the PDF metadata.
You should also retain the producer line in all PDFs created or modified using our
open-source.
</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="code" class="w-6 h-6 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-2">Code Modifications</h3>
<p class="text-gray-400 text-sm">
Any modifications to our open-source must be disclosed under the AGPL. This includes
sharing the modified code with any users interacting with it.
</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="check-circle-2" class="w-6 h-6 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-2">AGPL-Compliant Environments Only</h3>
<p class="text-gray-400 text-sm">
Usage of our open-source Community Edition must be limited to AGPL-compliant
environments, where all AGPL requirements are fully respected.
</p>
</div>
</div>
<div class="flex items-start gap-3">
<i data-lucide="git-branch" class="w-6 h-6 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-white font-semibold mb-2">Source Code Disclosure</h3>
<p class="text-gray-400 text-sm">
You cannot deploy our open-source as part of a server-based application or service
without disclosing your own application's full source code under AGPL to any users
interacting with it.
</p>
</div>
</div>
</div>
</div>
</section>
<div class="section-divider"></div>
<section id="agpl-compliance" class="py-16 max-w-6xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
What is <span class="marker-slanted">AGPL-Compliant?</span>
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- AGPL-Compliant -->
<div class="bg-green-900/20 border-2 border-green-500/30 rounded-xl p-6">
<div class="flex items-center gap-3 mb-4">
<i data-lucide="check-circle" class="w-8 h-8 text-green-400"></i>
<h3 class="text-2xl font-bold text-white">AGPL-Compliant</h3>
</div>
<ul class="space-y-3">
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5"></i>
<span class="text-gray-300 text-sm">Open-source projects where you share your full source
code publicly</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5"></i>
<span class="text-gray-300 text-sm">Personal use where you're not distributing or hosting it
as a service</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5"></i>
<span class="text-gray-300 text-sm">Publicly hosted tools where you provide the complete
source code (including modifications) under AGPL</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="check" class="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5"></i>
<span class="text-gray-300 text-sm">Educational or research projects that make all code
publicly available</span>
</li>
</ul>
</div>
<!-- NOT AGPL-Compliant -->
<div class="bg-red-900/20 border-2 border-red-500/30 rounded-xl p-6">
<div class="flex items-center gap-3 mb-4">
<i data-lucide="x-circle" class="w-8 h-8 text-red-400"></i>
<h3 class="text-2xl font-bold text-white">NOT AGPL-Compliant</h3>
</div>
<ul class="space-y-3">
<li class="flex items-start gap-2">
<i data-lucide="x" class="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5"></i>
<span class="text-gray-300 text-sm">Proprietary/closed-source SaaS applications</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="x" class="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5"></i>
<span class="text-gray-300 text-sm">Internal company tools where source code is not shared
with users</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="x" class="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5"></i>
<span class="text-gray-300 text-sm">Commercial products that don't disclose the full source
code</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="x" class="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5"></i>
<span class="text-gray-300 text-sm">Network services that don't provide source code access
to users</span>
</li>
<li class="flex items-start gap-2">
<i data-lucide="x" class="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5"></i>
<span class="text-gray-300 text-sm">Any application where you want to keep your source code
private</span>
</li>
</ul>
<p class="mt-4 text-sm text-yellow-400 italic">
⚠️ These use cases require a commercial license.
</p>
</div>
</div>
</section>
<div class="section-divider"></div>
<section id="licensing-faq" class="py-16 max-w-4xl mx-auto">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
Licensing <span class="marker-slanted">FAQs</span>
</h2>
<div id="faq-accordion" class="space-y-4">
<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 I need a commercial license if I'm using
BentoPDF on my
website?</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">
<p class="p-6 pt-0 text-gray-400">
If you're using BentoPDF on a public website and have not modified the
source code, you do not need a commercial license. However, if you modify
BentoPDF in any way and make it available to users over a network, the
AGPLv3 requires you to provide your modified source code to those users.
If you want to keep your modifications closed-source, or if you are
integrating BentoPDF into a proprietary or commercial product, you will
need a commercial license. Contact us to discuss your specific use case.
</p>
</div>
</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">Can I use BentoPDF for internal company
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">
<p class="p-6 pt-0 text-gray-400">
If your internal tool is closed-source and not shared publicly,
you will need a commercial license. The AGPL-3.0 license
requires that you make the source code of any network-accessible
service using BentoPDF available to its users. If you cannot or
do not want to do that, a commercial license is the right
choice.
</p>
</div>
</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">What if I modify BentoPDF for my own use?</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">
<p class="p-6 pt-0 text-gray-400">
If you modify BentoPDF and deploy it as a network service
(e.g., a web application), you must make your modified source
code available under AGPL-3.0. If you do not want to share your
modifications, you must obtain a commercial license.
</p>
</div>
</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">How do I purchase a commercial license?</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">
<p class="p-6 pt-0 text-gray-400">
BentoPDF is available under a lifetime, one-time purchase commercial license.
You can purchase it directly here:
<a href="https://ko-fi.com/s/f32ca4cb75" class="text-indigo-400 hover:underline">Buy
Commercial License</a>.
<br><br>
If you have specific requirements or want a custom licensing arrangement,
feel free to <a href="./contact.html" class="text-indigo-400 hover:underline">contact us</a>
with details about your use case, company size, and deployment needs.
We're happy to work with you to find the best fit.
</p>
</div>
</div>
</div>
</section>
<div class="section-divider"></div>
<section id="cta-section" class="text-center py-16">
<h2 class="text-3xl font-bold text-white mb-4">
Still have questions?
</h2>
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
We're here to help. Reach out to discuss your licensing needs.
</p>
<a href="./contact.html"
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">
Contact Us
</a>
</section>
</div>
<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="mb-8 md:mb-0">
<div class="flex items-center justify-center md:justify-start mb-4">
<img src="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">
&copy; 2025 BentoPDF. All rights reserved.
</p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span>
</p>
</div>
<div>
<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>
</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>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li>
<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>
</li>
</ul>
</div>
<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"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/AP2Y97juZT" 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" />
</svg>
</a>
<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">
<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">
<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" />
</svg>
</a>
</div>
</div>
</div>
</div>
</footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/faq.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body>
</html>
-2
View File
@@ -34,7 +34,5 @@ http {
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always; add_header X-XSS-Protection "1; mode=block" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always;
} }
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"version": "1.7.3", "version": "1.6.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bento-pdf", "name": "bento-pdf",
"version": "1.7.3", "version": "1.6.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@fontsource/cedarville-cursive": "^5.2.7", "@fontsource/cedarville-cursive": "^5.2.7",
+2 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"private": true, "private": true,
"version": "1.9.0", "version": "1.7.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -16,12 +16,10 @@
"test:watch": "vitest watch", "test:watch": "vitest watch",
"build:docker": "vite build", "build:docker": "vite build",
"format": "prettier --write .", "format": "prettier --write .",
"update-version": "node scripts/update-version.js",
"release": "node scripts/release.js patch", "release": "node scripts/release.js patch",
"release:minor": "node scripts/release.js minor", "release:minor": "node scripts/release.js minor",
"release:major": "node scripts/release.js major", "release:major": "node scripts/release.js major",
"serve:simple": "SIMPLE_MODE=true npm run build && npm run preview -- --port 3000", "serve:simple": "SIMPLE_MODE=true npm run build && npx serve dist -p 3000",
"serve": "npm run build && npm run preview -- --port 3000",
"package": "node scripts/package-dist.js" "package": "node scripts/package-dist.js"
}, },
"devDependencies": { "devDependencies": {
+66 -26
View File
@@ -1,6 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -12,8 +11,15 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> 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"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
@@ -24,25 +30,52 @@
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a> <a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center"> <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" 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> <span class="sr-only">Open main menu</span>
<!-- Hamburger Icon --> <!-- 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" <svg
stroke="currentColor" aria-hidden="true"> id="menu-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> 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> </svg>
<!-- Close Icon --> <!-- Close Icon -->
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" <svg
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> id="close-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> 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> </svg>
</button> </button>
</div> </div>
@@ -50,13 +83,17 @@
</div> </div>
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a> <a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="./licensing.html" class="mobile-nav-link">Licensing</a> <a href="index.html#tools-header" class="mobile-nav-link"
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a> >All Tools</a
>
</div> </div>
</div> </div>
</nav> </nav>
@@ -133,12 +170,19 @@
<p> <p>
We use privacy-respecting analytics platforms for this purpose. We use privacy-respecting analytics platforms for this purpose.
Specifically, we use Specifically, we use
<a href="https://simpleanalytics.com" target="_blank" rel="noopener noreferrer" <a
class="text-indigo-400 hover:underline">Simple Analytics</a> href="https://simpleanalytics.com"
target="_blank"
rel="noopener noreferrer"
class="text-indigo-400 hover:underline"
>Simple Analytics</a
>
to track anonymous visit counts. This means we can see how many to track anonymous visit counts. This means we can see how many
users visit our site, but users visit our site, but
<strong>we never collect personal information or identify individual <strong
users</strong>. Simple Analytics is fully GDPR-compliant and respects user >we never collect personal information or identify individual
users</strong
>. Simple Analytics is fully GDPR-compliant and respects user
privacy. We do not use tracking cookies for advertising or privacy. We do not use tracking cookies for advertising or
cross-site profiling. cross-site profiling.
</p> </p>
@@ -200,7 +244,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -222,9 +266,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -245,7 +286,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -272,8 +313,7 @@
</div> </div>
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
@@ -12372,10 +12372,7 @@ const PDFViewerApplication = {
let file; let file;
const queryString = document.location.search.substring(1); const queryString = document.location.search.substring(1);
const params = parseQueryString(queryString); const params = parseQueryString(queryString);
file = params.get("file"); file = params.get("file") ?? AppOptions.get("defaultUrl");
if (!file) {
file = "";
}
validateFileURL(file); validateFileURL(file);
const fileInput = this._openFileInput = document.createElement("input"); const fileInput = this._openFileInput = document.createElement("input");
fileInput.id = "fileInput"; fileInput.id = "fileInput";
+1 -1
View File
@@ -207,7 +207,7 @@
viewer: document.getElementById('viewer'), viewer: document.getElementById('viewer'),
eventBus, eventBus,
linkService, linkService,
annotationEditorMode: 1, annotationEditorMode: 0,
enableScripting: true, enableScripting: true,
renderer: 'canvas' renderer: 'canvas'
}); });
+2 -5
View File
@@ -1512,7 +1512,7 @@ class BaseExternalServices {
class BasePreferences { class BasePreferences {
#defaults = Object.freeze({ #defaults = Object.freeze({
altTextLearnMoreUrl: "", altTextLearnMoreUrl: "",
annotationEditorMode: 1, annotationEditorMode: 0,
annotationMode: 2, annotationMode: 2,
capCanvasAreaFactor: 200, capCanvasAreaFactor: 200,
commentLearnMoreUrl: "", commentLearnMoreUrl: "",
@@ -16429,10 +16429,7 @@ const PDFViewerApplication = {
let file; let file;
const queryString = document.location.search.substring(1); const queryString = document.location.search.substring(1);
const params = parseQueryString(queryString); const params = parseQueryString(queryString);
file = params.get("file"); file = params.get("file") ?? AppOptions.get("defaultUrl");
if (!file) {
file = "";
}
try { try {
file = new URL(decodeURIComponent(file)).href; file = new URL(decodeURIComponent(file)).href;
} catch { } catch {
-20
View File
@@ -1,20 +0,0 @@
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
interface AddAttachmentsMessage {
command: 'add-attachments';
pdfBuffer: ArrayBuffer;
attachmentBuffers: ArrayBuffer[];
attachmentNames: string[];
}
interface AddAttachmentsSuccessResponse {
status: 'success';
modifiedPDF: ArrayBuffer;
}
interface AddAttachmentsErrorResponse {
status: 'error';
message: string;
}
type AddAttachmentsResponse = AddAttachmentsSuccessResponse | AddAttachmentsErrorResponse;
-132
View File
@@ -1,132 +0,0 @@
self.importScripts('/coherentpdf.browser.min.js');
function parsePageRange(rangeString, totalPages) {
const pages = new Set();
const parts = rangeString.split(',').map(s => s.trim());
for (const part of parts) {
if (part.includes('-')) {
const [start, end] = part.split('-').map(s => parseInt(s.trim(), 10));
if (isNaN(start) || isNaN(end)) continue;
for (let i = Math.max(1, start); i <= Math.min(totalPages, end); i++) {
pages.add(i);
}
} else {
const pageNum = parseInt(part, 10);
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
pages.add(pageNum);
}
}
}
return Array.from(pages).sort((a, b) => a - b);
}
function addAttachmentsToPDFInWorker(pdfBuffer, attachmentBuffers, attachmentNames, attachmentLevel, pageRange) {
try {
const uint8Array = new Uint8Array(pdfBuffer);
let pdf;
try {
pdf = coherentpdf.fromMemory(uint8Array, '');
} catch (error) {
const errorMsg = error.message || error.toString();
if (errorMsg.includes('Failed to read PDF') ||
errorMsg.includes('Could not read object') ||
errorMsg.includes('No /Root entry') ||
errorMsg.includes('PDFError')) {
self.postMessage({
status: 'error',
message: 'The PDF file has structural issues and cannot be processed. The file may be corrupted, incomplete, or created with non-standard tools. Please try:\n\n• Opening and re-saving the PDF in another PDF viewer\n• Using a different PDF file\n• Repairing the PDF with a PDF repair tool'
});
} else {
self.postMessage({
status: 'error',
message: `Failed to load PDF: ${errorMsg}`
});
}
return;
}
const totalPages = coherentpdf.pages(pdf);
let targetPages = [];
if (attachmentLevel === 'page') {
if (!pageRange) {
self.postMessage({
status: 'error',
message: 'Page range is required for page-level attachments.'
});
coherentpdf.deletePdf(pdf);
return;
}
targetPages = parsePageRange(pageRange, totalPages);
if (targetPages.length === 0) {
self.postMessage({
status: 'error',
message: 'Invalid page range specified.'
});
coherentpdf.deletePdf(pdf);
return;
}
}
for (let i = 0; i < attachmentBuffers.length; i++) {
try {
const attachmentData = new Uint8Array(attachmentBuffers[i]);
const attachmentName = attachmentNames[i];
if (attachmentLevel === 'document') {
coherentpdf.attachFileFromMemory(attachmentData, attachmentName, pdf);
} else {
for (const pageNum of targetPages) {
coherentpdf.attachFileToPageFromMemory(attachmentData, attachmentName, pdf, pageNum);
}
}
} catch (error) {
console.warn(`Failed to attach file ${attachmentNames[i]}:`, error);
self.postMessage({
status: 'error',
message: `Failed to attach file ${attachmentNames[i]}: ${error.message || error}`
});
coherentpdf.deletePdf(pdf);
return;
}
}
// Save the modified PDF
const modifiedBytes = coherentpdf.toMemory(pdf, false, false);
coherentpdf.deletePdf(pdf);
const buffer = modifiedBytes.buffer.slice(
modifiedBytes.byteOffset,
modifiedBytes.byteOffset + modifiedBytes.byteLength
);
self.postMessage({
status: 'success',
modifiedPDF: buffer
}, [buffer]);
} catch (error) {
self.postMessage({
status: 'error',
message: error instanceof Error
? error.message
: 'Unknown error occurred while adding attachments.'
});
}
}
self.onmessage = (e) => {
if (e.data.command === 'add-attachments') {
addAttachmentsToPDFInWorker(
e.data.pdfBuffer,
e.data.attachmentBuffers,
e.data.attachmentNames,
e.data.attachmentLevel || 'document',
e.data.pageRange || ''
);
}
};
-63
View File
@@ -1,63 +0,0 @@
self.importScripts('/coherentpdf.browser.min.js');
self.onmessage = function (e) {
const { command, files } = e.data;
if (command === 'interleave') {
interleavePDFs(files);
}
};
function interleavePDFs(files) {
try {
const loadedPdfs = [];
const pageCounts = [];
for (const file of files) {
const uint8Array = new Uint8Array(file.data);
const pdfDoc = coherentpdf.fromMemory(uint8Array, "");
loadedPdfs.push(pdfDoc);
pageCounts.push(coherentpdf.pages(pdfDoc));
}
if (loadedPdfs.length < 2) {
throw new Error('At least two PDF files are required for interleaving.');
}
const maxPages = Math.max(...pageCounts);
const pdfsToMerge = [];
const rangesToMerge = [];
for (let i = 1; i <= maxPages; i++) {
for (let j = 0; j < loadedPdfs.length; j++) {
if (i <= pageCounts[j]) {
pdfsToMerge.push(loadedPdfs[j]);
rangesToMerge.push(coherentpdf.range(i, i));
}
}
}
if (pdfsToMerge.length === 0) {
throw new Error('No valid pages to merge.');
}
const mergedPdf = coherentpdf.mergeSame(pdfsToMerge, true, true, rangesToMerge);
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
const buffer = mergedPdfBytes.buffer;
coherentpdf.deletePdf(mergedPdf);
loadedPdfs.forEach(pdf => coherentpdf.deletePdf(pdf));
self.postMessage({
status: 'success',
pdfBytes: buffer
}, [buffer]);
} catch (error) {
self.postMessage({
status: 'error',
message: error.message || 'Unknown error during interleave merge'
});
}
}
-70
View File
@@ -1,70 +0,0 @@
self.importScripts('/coherentpdf.browser.min.js');
self.onmessage = function (e) {
const { command, files, jobs } = e.data;
if (command === 'merge') {
mergePDFs(files, jobs);
}
};
function mergePDFs(files, jobs) {
try {
const loadedPdfs = {};
const pdfsToMerge = [];
const rangesToMerge = [];
for (const file of files) {
const uint8Array = new Uint8Array(file.data);
const pdfDoc = coherentpdf.fromMemory(uint8Array, "");
loadedPdfs[file.name] = pdfDoc;
}
for (const job of jobs) {
const sourcePdf = loadedPdfs[job.fileName];
if (!sourcePdf) continue;
let range;
if (job.rangeType === 'all') {
range = coherentpdf.all(sourcePdf);
} else if (job.rangeType === 'specific') {
if (coherentpdf.validatePagespec(job.rangeString)) {
range = coherentpdf.parsePagespec(sourcePdf, job.rangeString);
} else {
range = coherentpdf.all(sourcePdf);
}
} else if (job.rangeType === 'single') {
const pageNum = job.pageIndex + 1;
range = coherentpdf.range(pageNum, pageNum);
} else if (job.rangeType === 'range') {
range = coherentpdf.range(job.startPage, job.endPage);
}
pdfsToMerge.push(sourcePdf);
rangesToMerge.push(range);
}
if (pdfsToMerge.length === 0) {
throw new Error('No valid files or pages to merge.');
}
const mergedPdf = coherentpdf.mergeSame(pdfsToMerge, true, true, rangesToMerge);
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
const buffer = mergedPdfBytes.buffer;
coherentpdf.deletePdf(mergedPdf);
Object.values(loadedPdfs).forEach(pdf => coherentpdf.deletePdf(pdf));
self.postMessage({
status: 'success',
pdfBytes: buffer
}, [buffer]);
} catch (error) {
self.postMessage({
status: 'error',
message: error.message || 'Unknown error during merge'
});
}
}
+5 -9
View File
@@ -75,24 +75,20 @@ function main() {
const newVersion = updateVersion(type); const newVersion = updateVersion(type);
console.log(`📦 Updated version to ${newVersion}`); console.log(`📦 Updated version to ${newVersion}`);
// 2. Update version in HTML files // 2. Add and commit changes
console.log(`📝 Updating version in HTML files...`); execSync('git add package.json', { stdio: 'inherit' });
execSync('npm run update-version', { stdio: 'inherit' });
// 3. Add and commit changes
execSync('git add package.json *.html src/pages/*.html', { stdio: 'inherit' });
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' }); execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
console.log(`💾 Committed version change`); console.log(`💾 Committed version change`);
// 4. Create git tag // 3. Create git tag
const tagName = createGitTag(newVersion); const tagName = createGitTag(newVersion);
// 5. Build and package the distribution files // 4. Build and package the distribution files
console.log(`📦 Building and packaging distribution files...`); console.log(`📦 Building and packaging distribution files...`);
execSync('npm run package', { stdio: 'inherit' }); execSync('npm run package', { stdio: 'inherit' });
console.log(`📦 Distribution files packaged successfully`); console.log(`📦 Distribution files packaged successfully`);
// 6. Push everything to main // 5. Push everything to main
console.log(`📤 Pushing to main...`); console.log(`📤 Pushing to main...`);
execSync('git push origin main', { stdio: 'inherit' }); execSync('git push origin main', { stdio: 'inherit' });
execSync(`git push origin ${tagName}`, { stdio: 'inherit' }); execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
-64
View File
@@ -1,64 +0,0 @@
#!/usr/bin/env node
/**
* Script to update version numbers in HTML files from package.json
* Run this script whenever you need to sync HTML versions with package.json
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Read version from package.json
const packageJson = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
);
const version = packageJson.version;
// HTML files to update
const htmlFiles = [
'index.html',
'about.html',
'contact.html',
'faq.html',
'privacy.html',
'terms.html',
'src/pages/add-stamps.html',
'src/pages/bookmark.html',
'src/pages/json-to-pdf.html',
'src/pages/pdf-multi-tool.html',
'src/pages/pdf-to-json.html',
'src/pages/table-of-contents.html',
];
console.log(`Updating version to ${version} in HTML files...`);
let updatedCount = 0;
htmlFiles.forEach((file) => {
const filePath = path.join(__dirname, '..', file);
if (!fs.existsSync(filePath)) {
console.log(`⚠️ Skipping ${file} (not found)`);
return;
}
let content = fs.readFileSync(filePath, 'utf8');
// Replace version in <span id="app-version">X.X.X</span>
const regex = /(<span id="app-version">)[^<]+(<\/span>)/g;
const newContent = content.replace(regex, `$1${version}$2`);
if (content !== newContent) {
fs.writeFileSync(filePath, newContent, 'utf8');
console.log(`✓ Updated ${file}`);
updatedCount++;
} else {
console.log(`- ${file} (already up to date)`);
}
});
console.log(`\nDone! Updated ${updatedCount} file(s) to version ${version}`);
-25
View File
@@ -1,25 +0,0 @@
{
"headers": [
{
"source": "**/*",
"headers": [
{
"key": "Cross-Origin-Opener-Policy",
"value": "same-origin"
},
{
"key": "Cross-Origin-Embedder-Policy",
"value": "require-corp"
},
{
"key": "X-Frame-Options",
"value": "SAMEORIGIN"
},
{
"key": "X-Content-Type-Options",
"value": "nosniff"
}
]
}
]
}
+1 -84
View File
@@ -257,11 +257,9 @@ footer a {
z-index: -1; z-index: -1;
} }
.section-divider { .section-divider {
height: 1px; height: 1px;
background: linear-gradient(to right, transparent, #4d44f7, transparent); background: linear-gradient(to right, transparent, #4f46e5, transparent);
/* Fades from transparent to indigo and back */ /* Fades from transparent to indigo and back */
margin: 2rem auto; margin: 2rem auto;
/* my-16 */ /* my-16 */
@@ -523,84 +521,3 @@ button:disabled,
.btn:disabled { .btn:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
/* Keyboard Shortcuts Styles */
.shortcut-key {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
height: 1.5rem;
padding: 0 0.25rem;
font-size: 0.75rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
line-height: 1;
color: #e5e7eb;
background-color: #374151;
border: 1px solid #4b5563;
border-radius: 0.25rem;
box-shadow: 0 1px 0 0 #1f2937;
}
.shortcut-input {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
letter-spacing: 0.05em;
}
.shortcut-input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
/* Custom Scrollbar for Modal Content */
#shortcuts-list::-webkit-scrollbar {
width: 6px;
}
#shortcuts-list::-webkit-scrollbar-track {
background: #1f2937;
}
#shortcuts-list::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 3px;
}
#shortcuts-list::-webkit-scrollbar-thumb:hover {
background: #6b7280;
}
/* Hide elements on touch devices */
@media (pointer: coarse) {
.hidden-on-touch {
display: none !important;
}
}
/* Also hide on very small screens just in case */
@media (max-width: 640px) {
.hidden-on-touch {
display: none !important;
}
}
/* Scroll to Top Button Visibility */
#scroll-to-top-btn.visible {
opacity: 1;
transform: translateY(0);
visibility: visible;
}
.bg-grid-pattern {
position: absolute;
inset: 0;
z-index: -1;
background-size: 60px 60px;
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
-webkit-mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
pointer-events: none;
}
+6 -25
View File
@@ -1,29 +1,9 @@
import { showLoader, hideLoader, showAlert } from './ui.js'; import { showLoader, hideLoader, showAlert } from './ui.js';
import { getPDFDocument } from './utils/helpers.js';
import { state } from './state.js'; import { state } from './state.js';
import { toolLogic } from './logic/index.js'; import { toolLogic } from './logic/index.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); const editorState = {
const editorState: {
pdf: any;
canvas: any;
context: any;
container: any;
currentPageNum: number;
pageRendering: boolean;
pageNumPending: number | null;
scale: number | 'fit';
pageSnapshot: any;
isDrawing: boolean;
startX: number;
startY: number;
cropBoxes: Record<number, any>;
lastInteractionRect: { x: number; y: number; width: number; height: number } | null;
} = {
pdf: null, pdf: null,
canvas: null, canvas: null,
context: null, context: null,
@@ -61,6 +41,7 @@ async function renderPage(num: any) {
try { try {
const page = await editorState.pdf.getPage(num); const page = await editorState.pdf.getPage(num);
// @ts-expect-error TS(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
if (editorState.scale === 'fit') { if (editorState.scale === 'fit') {
editorState.scale = calculateFitScale(page); editorState.scale = calculateFitScale(page);
} }
@@ -204,10 +185,12 @@ export async function setupCanvasEditor(toolId: any) {
const pageNav = document.getElementById('page-nav'); const pageNav = document.getElementById('page-nav');
const pdfData = await state.pdfDoc.save(); const pdfData = await state.pdfDoc.save();
editorState.pdf = await getPDFDocument({ data: pdfData }).promise; // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
editorState.cropBoxes = {}; editorState.cropBoxes = {};
editorState.currentPageNum = 1; editorState.currentPageNum = 1;
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
editorState.scale = 'fit'; editorState.scale = 'fit';
pageNav.textContent = ''; pageNav.textContent = '';
@@ -273,13 +256,11 @@ export async function setupCanvasEditor(toolId: any) {
if (toolId === 'crop') { if (toolId === 'crop') {
document.getElementById('zoom-in-btn').onclick = () => { document.getElementById('zoom-in-btn').onclick = () => {
if (typeof editorState.scale === 'number') {
editorState.scale += 0.25; editorState.scale += 0.25;
}
renderPage(editorState.currentPageNum); renderPage(editorState.currentPageNum);
}; };
document.getElementById('zoom-out-btn').onclick = () => { document.getElementById('zoom-out-btn').onclick = () => {
if (typeof editorState.scale === 'number' && editorState.scale > 0.25) { if (editorState.scale > 0.25) {
editorState.scale -= 0.25; editorState.scale -= 0.25;
renderPage(editorState.currentPageNum); renderPage(editorState.currentPageNum);
} }
+1 -2
View File
@@ -9,7 +9,7 @@ export const singlePdfLoadTools = [
'pdf-to-greyscale', 'pdf-to-greyscale',
'edit-metadata', 'edit-metadata',
'remove-metadata', 'remove-metadata',
'flatten',
'delete-pages', 'delete-pages',
'add-blank-page', 'add-blank-page',
'extract-pages', 'extract-pages',
@@ -68,5 +68,4 @@ export const multiFileTools = [
'txt-to-pdf', 'txt-to-pdf',
'compress', 'compress',
'extract-attachments', 'extract-attachments',
'flatten',
]; ];
+3 -9
View File
@@ -13,7 +13,7 @@ export const categories = [
id: 'merge', id: 'merge',
name: 'Merge PDF', name: 'Merge PDF',
icon: 'combine', icon: 'combine',
subtitle: 'Combine multiple PDFs into one file. Preserves Bookmarks.', subtitle: 'Combine multiple PDFs into one file.',
}, },
{ {
id: 'split', id: 'split',
@@ -161,12 +161,6 @@ export const categories = [
icon: 'square-pen', icon: 'square-pen',
subtitle: 'Fill in forms directly in the browser. Also supports XFA forms.', subtitle: 'Fill in forms directly in the browser. Also supports XFA forms.',
}, },
{
href: '/src/pages/form-creator.html',
name: 'Create PDF Form',
icon: 'file-input',
subtitle: 'Create fillable PDF forms with drag-and-drop text fields.',
},
{ {
id: 'remove-blank-pages', id: 'remove-blank-pages',
name: 'Remove Blank Pages', name: 'Remove Blank Pages',
@@ -182,7 +176,7 @@ export const categories = [
id: 'image-to-pdf', id: 'image-to-pdf',
name: 'Image to PDF', name: 'Image to PDF',
icon: 'images', icon: 'images',
subtitle: 'Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF.', subtitle: 'Combine various images into one PDF.',
}, },
{ {
id: 'jpg-to-pdf', id: 'jpg-to-pdf',
@@ -310,7 +304,7 @@ export const categories = [
id: 'alternate-merge', id: 'alternate-merge',
name: 'Alternate & Mix Pages', name: 'Alternate & Mix Pages',
icon: 'shuffle', icon: 'shuffle',
subtitle: 'Merge PDFs by alternating pages from each PDF. Preserves Bookmarks', subtitle: 'Combine PDFs by alternating pages from each.',
}, },
{ {
id: 'organize', id: 'organize',
-23
View File
@@ -1,23 +0,0 @@
// Simple FAQ accordion handler for standalone pages
document.addEventListener('DOMContentLoaded', () => {
const faqAccordion = document.getElementById('faq-accordion');
if (faqAccordion) {
faqAccordion.addEventListener('click', (e) => {
const questionButton = (e.target as HTMLElement).closest('.faq-question');
if (!questionButton) return;
const faqItem = questionButton.parentElement;
const answer = faqItem?.querySelector('.faq-answer') as HTMLElement;
if (!faqItem || !answer) return;
faqItem.classList.toggle('open');
if (faqItem.classList.contains('open')) {
answer.style.maxHeight = answer.scrollHeight + 'px';
} else {
answer.style.maxHeight = '0px';
}
});
}
});
+12 -133
View File
@@ -7,7 +7,7 @@ import {
renderFileDisplay, renderFileDisplay,
switchView, switchView,
} from '../ui.js'; } from '../ui.js';
import { formatIsoDate, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { formatIsoDate, readFileAsArrayBuffer } from '../utils/helpers.js';
import { setupCanvasEditor } from '../canvasEditor.js'; import { setupCanvasEditor } from '../canvasEditor.js';
import { toolLogic } from '../logic/index.js'; import { toolLogic } from '../logic/index.js';
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js'; import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
@@ -21,26 +21,6 @@ import {
} from '../config/pdf-tools.js'; } from '../config/pdf-tools.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const rotationState: number[] = [];
let imageSortableInstance: Sortable | null = null;
const activeImageUrls = new Map<File, string>();
export function getRotationState(): readonly number[] {
return rotationState;
}
export function updateRotationState(pageIndex: number, rotation: number) {
if (pageIndex >= 0 && pageIndex < rotationState.length) {
rotationState[pageIndex] = rotation;
}
}
export function resetRotationState() {
rotationState.length = 0;
}
async function handleSinglePdfUpload(toolId, file) { async function handleSinglePdfUpload(toolId, file) {
showLoader('Loading PDF...'); showLoader('Loading PDF...');
try { try {
@@ -121,12 +101,6 @@ async function handleSinglePdfUpload(toolId, file) {
await renderPageThumbnails(toolId, state.pdfDoc); await renderPageThumbnails(toolId, state.pdfDoc);
if (toolId === 'rotate') { if (toolId === 'rotate') {
// Initialize rotation state for all pages
rotationState.length = 0;
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
rotationState.push(0);
}
const rotateAllControls = document.getElementById( const rotateAllControls = document.getElementById(
'rotate-all-controls' 'rotate-all-controls'
); );
@@ -139,15 +113,11 @@ async function handleSinglePdfUpload(toolId, file) {
createIcons({ icons }); createIcons({ icons });
const rotateAll = (direction) => { const rotateAll = (direction) => {
// Update rotation state for ALL pages (including unrendered ones)
for (let i = 0; i < rotationState.length; i++) {
rotationState[i] = (rotationState[i] + direction * 90 + 360) % 360;
}
// Update DOM for currently rendered pages
document.querySelectorAll('.page-rotator-item').forEach((item) => { document.querySelectorAll('.page-rotator-item').forEach((item) => {
const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0'); const currentRotation = parseInt(
const newRotation = rotationState[pageIndex]; (item as HTMLElement).dataset.rotation || '0'
);
const newRotation = (currentRotation + direction * 90 + 360) % 360;
(item as HTMLElement).dataset.rotation = newRotation.toString(); (item as HTMLElement).dataset.rotation = newRotation.toString();
const thumbnail = item.querySelector('canvas, img'); const thumbnail = item.querySelector('canvas, img');
if (thumbnail) { if (thumbnail) {
@@ -174,7 +144,7 @@ async function handleSinglePdfUpload(toolId, file) {
try { try {
const pdfBytes = await readFileAsArrayBuffer(state.files[0]); const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
const pdfjsDoc = await getPDFDocument({ const pdfjsDoc = await pdfjsLib.getDocument({
data: pdfBytes as ArrayBuffer, data: pdfBytes as ArrayBuffer,
}).promise; }).promise;
const [metadataResult, fieldObjects] = await Promise.all([ const [metadataResult, fieldObjects] = await Promise.all([
@@ -479,43 +449,6 @@ async function handleSinglePdfUpload(toolId, file) {
toolLogic['page-dimensions'](); toolLogic['page-dimensions']();
} }
// Setup quality sliders for image conversion tools
if (toolId === 'pdf-to-jpg') {
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
const qualityValue = document.getElementById('jpg-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
};
qualitySlider.addEventListener('input', updateValue);
updateValue();
}
}
if (toolId === 'pdf-to-png') {
const qualitySlider = document.getElementById('png-quality') as HTMLInputElement;
const qualityValue = document.getElementById('png-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
qualityValue.textContent = `${qualitySlider.value}x`;
};
qualitySlider.addEventListener('input', updateValue);
updateValue();
}
}
if (toolId === 'pdf-to-webp') {
const qualitySlider = document.getElementById('webp-quality') as HTMLInputElement;
const qualityValue = document.getElementById('webp-quality-value');
if (qualitySlider && qualityValue) {
const updateValue = () => {
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
};
qualitySlider.addEventListener('input', updateValue);
updateValue();
}
}
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') { if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
toolLogic[toolId].setup(); toolLogic[toolId].setup();
} }
@@ -535,8 +468,6 @@ async function handleMultiFileUpload(toolId) {
toolId === 'alternate-merge' || toolId === 'alternate-merge' ||
toolId === 'reverse-pages' toolId === 'reverse-pages'
) { ) {
showLoader('Loading PDF documents...');
const pdfFilesUnloaded: File[] = []; const pdfFilesUnloaded: File[] = [];
state.files.forEach((file) => { state.files.forEach((file) => {
@@ -571,7 +502,6 @@ async function handleMultiFileUpload(toolId) {
const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`; const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`;
hideLoader(); // Hide loader before showing alert
showAlert('Protected PDFs', errorMessage); showAlert('Protected PDFs', errorMessage);
switchView('grid'); switchView('grid');
@@ -596,27 +526,9 @@ async function handleMultiFileUpload(toolId) {
toolLogic['alternate-merge'].setup(); toolLogic['alternate-merge'].setup();
} else if (toolId === 'image-to-pdf') { } else if (toolId === 'image-to-pdf') {
const imageList = document.getElementById('image-list'); const imageList = document.getElementById('image-list');
imageList.textContent = '';
const renderedFiles = new Set(
Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName)
);
state.files.forEach((file) => { state.files.forEach((file) => {
if (!file) { const url = URL.createObjectURL(file);
console.error('Invalid file encountered in state.files');
return;
}
if (renderedFiles.has(file.name)) {
return;
}
let url = activeImageUrls.get(file);
if (!url) {
url = URL.createObjectURL(file);
activeImageUrls.set(file, url);
}
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'relative group cursor-move'; li.className = 'relative group cursor-move';
li.dataset.fileName = file.name; li.dataset.fileName = file.name;
@@ -638,28 +550,10 @@ async function handleMultiFileUpload(toolId) {
imageList.appendChild(li); imageList.appendChild(li);
}); });
const syncStateWithDOM = () => { Sortable.create(imageList);
const domOrder = Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName); // Show image-to-pdf options and wire up slider text
state.files.sort((a, b) => {
const aIndex = domOrder.indexOf(a.name);
const bIndex = domOrder.indexOf(b.name);
return aIndex - bIndex;
});
};
if (!imageSortableInstance) {
imageSortableInstance = Sortable.create(imageList, {
animation: 150,
onEnd: () => {
syncStateWithDOM();
}
});
}
syncStateWithDOM();
const opts = document.getElementById('image-to-pdf-options'); const opts = document.getElementById('image-to-pdf-options');
if (opts && opts.classList.contains('hidden')) { if (opts) {
opts.classList.remove('hidden'); opts.classList.remove('hidden');
const slider = document.getElementById('image-pdf-quality') as HTMLInputElement; const slider = document.getElementById('image-pdf-quality') as HTMLInputElement;
const value = document.getElementById('image-pdf-quality-value'); const value = document.getElementById('image-pdf-quality-value');
@@ -723,18 +617,6 @@ export function setupFileInputHandler(toolId) {
const processFiles = async (newFiles) => { const processFiles = async (newFiles) => {
if (newFiles.length === 0) return; if (newFiles.length === 0) return;
if (toolId === 'image-to-pdf') {
const validTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/bmp', 'image/tiff'];
const validFiles = newFiles.filter(file => validTypes.includes(file.type));
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped because they are not supported images.');
}
newFiles = validFiles;
if (newFiles.length === 0) return;
}
if (!isMultiFileTool || isFirstUpload) { if (!isMultiFileTool || isFirstUpload) {
state.files = newFiles; state.files = newFiles;
} else { } else {
@@ -754,7 +636,7 @@ export function setupFileInputHandler(toolId) {
} }
if (isMultiFileTool) { if (isMultiFileTool) {
if (toolId === 'txt-to-pdf' || toolId === 'compress' || toolId === 'extract-attachments' || toolId === 'flatten') { if (toolId === 'txt-to-pdf' || toolId === 'compress' || toolId === 'extract-attachments') {
const processBtn = document.getElementById('process-btn'); const processBtn = document.getElementById('process-btn');
if (processBtn) { if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false; (processBtn as HTMLButtonElement).disabled = false;
@@ -849,9 +731,6 @@ export function setupFileInputHandler(toolId) {
const clearBtn = document.getElementById('clear-files-btn'); const clearBtn = document.getElementById('clear-files-btn');
if (clearBtn) { if (clearBtn) {
clearBtn.addEventListener('click', () => { clearBtn.addEventListener('click', () => {
activeImageUrls.forEach(url => URL.revokeObjectURL(url));
activeImageUrls.clear();
state.files = []; state.files = [];
isFirstUpload = true; isFirstUpload = true;
(fileInput as HTMLInputElement).value = ''; (fileInput as HTMLInputElement).value = '';
+26 -113
View File
@@ -1,40 +1,10 @@
import { showLoader, hideLoader, showAlert } from '../ui'; import { showLoader, hideLoader, showAlert } from '../ui';
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers'; import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
import { state } from '../state'; import { state } from '../state';
const worker = new Worker('/workers/add-attachments.worker.js');
let attachments: File[] = []; let attachments: File[] = [];
worker.onmessage = (e) => {
const data = e.data;
if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`attached-${state.files[0].name}`
);
showAlert('Success', `${attachments.length} file(s) attached successfully.`);
clearAttachments();
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
clearAttachments();
}
};
worker.onerror = (error) => {
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
clearAttachments();
};
export async function addAttachments() { export async function addAttachments() {
if (!state.files || state.files.length === 0) { if (!state.pdfDoc) {
showAlert('Error', 'Main PDF is not loaded.'); showAlert('Error', 'Main PDF is not loaded.');
return; return;
} }
@@ -43,57 +13,39 @@ export async function addAttachments() {
return; return;
} }
const attachmentLevel = (
document.querySelector('input[name="attachment-level"]:checked') as HTMLInputElement
)?.value || 'document';
let pageRange: string = '';
if (attachmentLevel === 'page') {
const pageRangeInput = document.getElementById('attachment-page-range') as HTMLInputElement;
pageRange = pageRangeInput?.value?.trim() || '';
if (!pageRange) {
showAlert('Error', 'Please specify a page range for page-level attachments.');
return;
}
}
showLoader('Embedding files into PDF...'); showLoader('Embedding files into PDF...');
try { try {
const pdfFile = state.files[0]; const pdfDoc = state.pdfDoc;
const pdfBuffer = (await readFileAsArrayBuffer(pdfFile)) as ArrayBuffer;
const attachmentBuffers: ArrayBuffer[] = [];
const attachmentNames: string[] = [];
for (let i = 0; i < attachments.length; i++) { for (let i = 0; i < attachments.length; i++) {
const file = attachments[i]; const file = attachments[i];
showLoader(`Reading ${file.name} (${i + 1}/${attachments.length})...`); showLoader(`Attaching ${file.name} (${i + 1}/${attachments.length})...`);
const fileBuffer = (await readFileAsArrayBuffer(file)) as ArrayBuffer; const fileBytes = await readFileAsArrayBuffer(file);
attachmentBuffers.push(fileBuffer);
attachmentNames.push(file.name); await pdfDoc.attach(fileBytes as ArrayBuffer, file.name, {
mimeType: file.type || 'application/octet-stream',
description: `Attached file: ${file.name}`,
creationDate: new Date(),
modificationDate: new Date(file.lastModified),
});
} }
showLoader('Attaching files to PDF...'); const pdfBytes = await pdfDoc.save();
downloadFile(
const message = { new Blob([pdfBytes], { type: 'application/pdf' }),
command: 'add-attachments', `attached-${state.files[0].name}`
pdfBuffer: pdfBuffer, );
attachmentBuffers: attachmentBuffers,
attachmentNames: attachmentNames,
attachmentLevel: attachmentLevel,
pageRange: pageRange
};
const transferables = [pdfBuffer, ...attachmentBuffers];
worker.postMessage(message, transferables);
showAlert(
'Success',
`${attachments.length} file(s) attached successfully.`
);
} catch (error: any) { } catch (error: any) {
console.error('Error attaching files:', error); console.error('Error attaching files:', error);
hideLoader();
showAlert('Error', `Failed to attach files: ${error.message}`); showAlert('Error', `Failed to attach files: ${error.message}`);
} finally {
hideLoader();
clearAttachments(); clearAttachments();
} }
} }
@@ -107,8 +59,6 @@ function clearAttachments() {
const processBtn = document.getElementById( const processBtn = document.getElementById(
'process-btn' 'process-btn'
) as HTMLButtonElement; ) as HTMLButtonElement;
const attachmentLevelOptions = document.getElementById('attachment-level-options');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
if (fileListDiv) fileListDiv.innerHTML = ''; if (fileListDiv) fileListDiv.innerHTML = '';
if (attachmentInput) attachmentInput.value = ''; if (attachmentInput) attachmentInput.value = '';
@@ -116,20 +66,14 @@ function clearAttachments() {
processBtn.disabled = true; processBtn.disabled = true;
processBtn.classList.add('hidden'); processBtn.classList.add('hidden');
} }
if (attachmentLevelOptions) {
attachmentLevelOptions.classList.add('hidden');
}
if (pageRangeWrapper) {
pageRangeWrapper.classList.add('hidden');
} }
const documentRadio = document.querySelector('input[name="attachment-level"][value="document"]') as HTMLInputElement; let isSetup = false; // Prevent duplicate setup
if (documentRadio) {
documentRadio.checked = true;
}
}
export function setupAddAttachmentsTool() { export function setupAddAttachmentsTool() {
if (isSetup) return; // Already set up
isSetup = true;
const optionsDiv = document.getElementById('attachment-options'); const optionsDiv = document.getElementById('attachment-options');
const attachmentInput = document.getElementById( const attachmentInput = document.getElementById(
'attachment-files-input' 'attachment-files-input'
@@ -138,29 +82,14 @@ export function setupAddAttachmentsTool() {
const processBtn = document.getElementById( const processBtn = document.getElementById(
'process-btn' 'process-btn'
) as HTMLButtonElement; ) as HTMLButtonElement;
const attachmentLevelOptions = document.getElementById('attachment-level-options');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
const totalPagesSpan = document.getElementById('attachment-total-pages');
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) { if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
console.error('Attachment tool UI elements not found.'); console.error('Attachment tool UI elements not found.');
return; return;
} }
if (!state.files || state.files.length === 0) {
console.error('No PDF file loaded for adding attachments.');
return;
}
optionsDiv.classList.remove('hidden'); optionsDiv.classList.remove('hidden');
if (totalPagesSpan && state.pdfDoc) {
totalPagesSpan.textContent = state.pdfDoc.getPageCount().toString();
}
if (attachmentInput.dataset.listenerAttached) return;
attachmentInput.dataset.listenerAttached = 'true';
attachmentInput.addEventListener('change', (e) => { attachmentInput.addEventListener('change', (e) => {
const files = (e.target as HTMLInputElement).files; const files = (e.target as HTMLInputElement).files;
if (files && files.length > 0) { if (files && files.length > 0) {
@@ -185,10 +114,6 @@ export function setupAddAttachmentsTool() {
fileListDiv.appendChild(div); fileListDiv.appendChild(div);
}); });
if (attachmentLevelOptions) {
attachmentLevelOptions.classList.remove('hidden');
}
processBtn.disabled = false; processBtn.disabled = false;
processBtn.classList.remove('hidden'); processBtn.classList.remove('hidden');
} else { } else {
@@ -196,17 +121,5 @@ export function setupAddAttachmentsTool() {
} }
}); });
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
attachmentLevelRadios.forEach((radio) => {
radio.addEventListener('change', (e) => {
const value = (e.target as HTMLInputElement).value;
if (value === 'page' && pageRangeWrapper) {
pageRangeWrapper.classList.remove('hidden');
} else if (pageRangeWrapper) {
pageRangeWrapper.classList.add('hidden');
}
});
});
processBtn.onclick = addAttachments; processBtn.onclick = addAttachments;
} }
+5 -24
View File
@@ -1,5 +1,4 @@
import { formatBytes, readFileAsArrayBuffer } from '../utils/helpers' import { formatBytes, readFileAsArrayBuffer } from '../utils/helpers'
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'
let selectedFile: File | null = null let selectedFile: File | null = null
let viewerIframe: HTMLIFrameElement | null = null let viewerIframe: HTMLIFrameElement | null = null
@@ -59,23 +58,11 @@ async function loadPdfInViewer(file: File) {
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' }) const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' })
currentBlobUrl = URL.createObjectURL(blob) currentBlobUrl = URL.createObjectURL(blob)
try {
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences')
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {}
delete (existingPrefs as any).annotationEditorMode
const newPrefs = {
...existingPrefs,
enablePermissions: false,
}
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs))
} catch { }
const iframe = document.createElement('iframe') const iframe = document.createElement('iframe')
iframe.className = 'w-full h-full border-0' iframe.className = 'w-full h-full border-0'
iframe.allowFullscreen = true iframe.allowFullscreen = true
const viewerUrl = new URL('/pdfjs-annotation-viewer/web/viewer.html', window.location.origin) iframe.src = `/pdfjs-annotation-viewer/web/viewer.html?file=${encodeURIComponent(currentBlobUrl)}`
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}`
iframe.addEventListener('load', () => { iframe.addEventListener('load', () => {
setupAnnotationViewer(iframe) setupAnnotationViewer(iframe)
@@ -98,14 +85,10 @@ function setupAnnotationViewer(iframe: HTMLIFrameElement) {
await app.initializedPromise await app.initializedPromise
} }
const eventBus = app?.eventBus const stampBtn = doc.getElementById('editorStamp') as HTMLButtonElement | null
if (eventBus && typeof eventBus._on === 'function') { if (stampBtn) {
eventBus._on('annotationeditoruimanager', () => { stampBtn.classList.remove('hidden')
try { stampBtn.disabled = false
const stampBtn = doc.getElementById('editorStampButton') as HTMLButtonElement | null
stampBtn?.click()
} catch { }
})
} }
const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null const root = doc.querySelector('.PdfjsAnnotationExtension') as HTMLElement | null
@@ -175,5 +158,3 @@ if (backToToolsBtn) {
window.location.href = '/' window.location.href = '/'
}) })
} }
initializeGlobalShortcuts()
+8 -2
View File
@@ -3,7 +3,6 @@ import {
downloadFile, downloadFile,
readFileAsArrayBuffer, readFileAsArrayBuffer,
hexToRgb, hexToRgb,
resetAndReloadTool,
} from '../utils/helpers.js'; } from '../utils/helpers.js';
import { state, resetState } from '../state.js'; import { state, resetState } from '../state.js';
@@ -176,7 +175,14 @@ export async function addWatermark() {
'watermarked.pdf' 'watermarked.pdf'
); );
resetAndReloadTool(); const toolid = state.activeTool;
resetState();
if (toolid) {
const element = document.querySelector(
`[data-tool-id="${toolid}"]`
) as HTMLElement;
if (element) element.click();
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert( showAlert(
+31 -49
View File
@@ -1,15 +1,13 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument } from 'pdf-lib';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
const alternateMergeState = { const alternateMergeState = {
pdfDocs: {} as Record<string, any>, pdfDocs: {},
pdfBytes: {} as Record<string, ArrayBuffer>,
}; };
const alternateMergeWorker = new Worker('/workers/alternate-merge.worker.js');
export async function setupAlternateMergeTool() { export async function setupAlternateMergeTool() {
const optionsDiv = document.getElementById('alternate-merge-options'); const optionsDiv = document.getElementById('alternate-merge-options');
const processBtn = document.getElementById( const processBtn = document.getElementById(
@@ -25,18 +23,18 @@ export async function setupAlternateMergeTool() {
fileList.innerHTML = ''; fileList.innerHTML = '';
alternateMergeState.pdfDocs = {}; alternateMergeState.pdfDocs = {};
alternateMergeState.pdfBytes = {};
showLoader('Loading PDF documents...'); showLoader('Loading PDF documents...');
try { try {
for (const file of state.files) { for (const file of state.files) {
const pdfBytes = await readFileAsArrayBuffer(file); const pdfBytes = await readFileAsArrayBuffer(file);
alternateMergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer; alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(
pdfBytes as ArrayBuffer,
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0); {
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise; ignoreEncryption: true,
alternateMergeState.pdfDocs[file.name] = pdfjsDoc; }
const pageCount = pdfjsDoc.numPages; );
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
const li = document.createElement('li'); const li = document.createElement('li');
li.className = li.className =
@@ -81,7 +79,7 @@ export async function setupAlternateMergeTool() {
} }
export async function alternateMerge() { export async function alternateMerge() {
if (Object.keys(alternateMergeState.pdfBytes).length < 2) { if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
showAlert( showAlert(
'Not Enough Files', 'Not Enough Files',
'Please upload at least two PDF files to alternate and mix.' 'Please upload at least two PDF files to alternate and mix.'
@@ -91,53 +89,37 @@ export async function alternateMerge() {
showLoader('Alternating and mixing pages...'); showLoader('Alternating and mixing pages...');
try { try {
const newPdfDoc = await PDFDocument.create();
const fileList = document.getElementById('alternate-file-list'); const fileList = document.getElementById('alternate-file-list');
if (!fileList) throw new Error('File list not found');
const sortedFileNames = Array.from(fileList.children).map( const sortedFileNames = Array.from(fileList.children).map(
(li) => (li as HTMLElement).dataset.fileName (li) => (li as HTMLElement).dataset.fileName
).filter(Boolean) as string[]; );
const filesToMerge: { name: string; data: ArrayBuffer }[] = []; const loadedDocs = sortedFileNames.map(
for (const name of sortedFileNames) { (name) => alternateMergeState.pdfDocs[name]
const bytes = alternateMergeState.pdfBytes[name]; );
if (bytes) { const pageCounts = loadedDocs.map((doc) => doc.getPageCount());
filesToMerge.push({ name, data: bytes }); const maxPages = Math.max(...pageCounts);
for (let i = 0; i < maxPages; i++) {
for (const doc of loadedDocs) {
if (i < doc.getPageCount()) {
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
newPdfDoc.addPage(copiedPage);
}
} }
} }
if (filesToMerge.length < 2) { const mergedPdfBytes = await newPdfDoc.save();
showAlert('Error', 'At least two valid PDFs are required.'); downloadFile(
hideLoader(); new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
return; 'alternated-mixed.pdf'
} );
alternateMergeWorker.postMessage({
command: 'interleave',
files: filesToMerge
}, filesToMerge.map(f => f.data));
alternateMergeWorker.onmessage = (e) => {
hideLoader();
if (e.data.status === 'success') {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
downloadFile(blob, 'alternated-mixed.pdf');
showAlert('Success', 'PDFs have been mixed successfully!'); showAlert('Success', 'PDFs have been mixed successfully!');
} else {
console.error('Worker interleave error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
}
};
alternateMergeWorker.onerror = (e) => {
hideLoader();
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) { } catch (e) {
console.error('Alternate Merge error:', e); console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.'); showAlert('Error', 'An error occurred while mixing the PDFs.');
} finally {
hideLoader(); hideLoader();
} }
} }
+43 -92
View File
@@ -6,11 +6,11 @@ import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import '../../css/bookmark.css'; import '../../css/bookmark.css';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
import { truncateFilename, getPDFDocument } from '../utils/helpers.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const modalContainer = document.getElementById('modal-container'); const modalContainer = document.getElementById('modal-container');
@@ -453,8 +453,6 @@ function cancelDestinationPicking() {
// Setup canvas click handler for destination picking // Setup canvas click handler for destination picking
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initializeGlobalShortcuts();
const canvas = document.getElementById('pdf-canvas'); const canvas = document.getElementById('pdf-canvas');
const canvasWrapper = document.getElementById('pdf-canvas-wrapper'); const canvasWrapper = document.getElementById('pdf-canvas-wrapper');
const cancelPickingBtn = document.getElementById('cancel-picking-btn'); const cancelPickingBtn = document.getElementById('cancel-picking-btn');
@@ -714,10 +712,8 @@ function handleResize() {
if (window.innerWidth >= 1024) { if (window.innerWidth >= 1024) {
viewerSection.classList.remove('hidden'); viewerSection.classList.remove('hidden');
bookmarksSection.classList.remove('hidden'); bookmarksSection.classList.remove('hidden');
showViewerBtn.classList.remove('bg-indigo-600', 'text-white'); showViewerBtn.classList.remove('bg-blue-50', 'text-blue-600');
showViewerBtn.classList.add('text-gray-300'); showBookmarksBtn.classList.remove('bg-blue-50', 'text-blue-600');
showBookmarksBtn.classList.remove('bg-indigo-600', 'text-white');
showBookmarksBtn.classList.add('text-gray-300');
} }
} }
@@ -726,19 +722,15 @@ window.addEventListener('resize', handleResize);
showViewerBtn.addEventListener('click', () => { showViewerBtn.addEventListener('click', () => {
viewerSection.classList.remove('hidden'); viewerSection.classList.remove('hidden');
bookmarksSection.classList.add('hidden'); bookmarksSection.classList.add('hidden');
showViewerBtn.classList.add('bg-indigo-600', 'text-white'); showViewerBtn.classList.add('bg-blue-50', 'text-blue-600');
showViewerBtn.classList.remove('text-gray-300'); showBookmarksBtn.classList.remove('bg-blue-50', 'text-blue-600');
showBookmarksBtn.classList.remove('bg-indigo-600', 'text-white');
showBookmarksBtn.classList.add('text-gray-300');
}); });
showBookmarksBtn.addEventListener('click', () => { showBookmarksBtn.addEventListener('click', () => {
viewerSection.classList.add('hidden'); viewerSection.classList.add('hidden');
bookmarksSection.classList.remove('hidden'); bookmarksSection.classList.remove('hidden');
showBookmarksBtn.classList.add('bg-indigo-600', 'text-white'); showBookmarksBtn.classList.add('bg-blue-50', 'text-blue-600');
showBookmarksBtn.classList.remove('text-gray-300'); showViewerBtn.classList.remove('bg-blue-50', 'text-blue-600');
showViewerBtn.classList.remove('bg-indigo-600', 'text-white');
showViewerBtn.classList.add('text-gray-300');
}); });
// Dropdown toggles // Dropdown toggles
@@ -868,10 +860,8 @@ function resetToUploader() {
// Reset mobile view // Reset mobile view
viewerSection.classList.remove('hidden'); viewerSection.classList.remove('hidden');
bookmarksSection.classList.add('hidden'); bookmarksSection.classList.add('hidden');
showViewerBtn.classList.add('bg-indigo-600', 'text-white'); showViewerBtn.classList.add('bg-blue-50', 'text-blue-600');
showViewerBtn.classList.remove('text-gray-300'); showBookmarksBtn.classList.remove('bg-blue-50', 'text-blue-600');
showBookmarksBtn.classList.remove('bg-indigo-600', 'text-white');
showBookmarksBtn.classList.add('text-gray-300');
} }
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
@@ -1040,7 +1030,7 @@ async function loadPDF(e) {
if (!file) return; if (!file) return;
originalFileName = file.name.replace('.pdf', ''); originalFileName = file.name.replace('.pdf', '');
filenameDisplay.textContent = truncateFilename(file.name); filenameDisplay.textContent = originalFileName;
renderFileDisplay(file); renderFileDisplay(file);
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
@@ -1053,7 +1043,7 @@ async function loadPDF(e) {
pdfLibDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true }); pdfLibDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
const loadingTask = getPDFDocument({ const loadingTask = pdfjsLib.getDocument({
data: new Uint8Array(arrayBuffer), data: new Uint8Array(arrayBuffer),
}); });
pdfJsDoc = await loadingTask.promise; pdfJsDoc = await loadingTask.promise;
@@ -1908,64 +1898,45 @@ async function extractExistingBookmarks(doc) {
console.error('Error building named destinations:', e); console.error('Error building named destinations:', e);
} }
function findPageIndex(pageRef, resolvedPageRef = null) { function findPageIndex(pageRef) {
if (!pageRef) return 0; if (!pageRef) return 0;
try { try {
// Method 1: Try the resolved page ref first (if provided)
if (resolvedPageRef) {
// Check if resolved is a page dictionary - compare to each page's dictionary
for (let i = 0; i < pages.length; i++) {
const pageDict = doc.context.lookup(pages[i].ref);
if (pageDict === resolvedPageRef) {
return i;
}
}
}
// Method 2: Direct PDFRef comparison
const directMatch = pages.findIndex((p) => p.ref === pageRef);
if (directMatch !== -1) {
return directMatch;
}
// Method 3: Try resolving if not already resolved
if (!resolvedPageRef) {
const resolved = resolveRef(pageRef); const resolved = resolveRef(pageRef);
if (resolved) {
// Check if the resolved object matches any page's dictionary
for (let i = 0; i < pages.length; i++) {
const pageDict = doc.context.lookup(pages[i].ref);
if (pageDict === resolved) {
return i;
}
}
}
}
// Method 4: Compare by string representation
if (pageRef.toString) {
const target = pageRef.toString();
const stringMatch = pages.findIndex(
(p) => p.ref && p.ref.toString() === target
);
if (stringMatch !== -1) {
return stringMatch;
}
}
// Method 5: Try numeric value (for edge cases)
if (pageRef.numberValue !== undefined) { if (pageRef.numberValue !== undefined) {
const numericIndex = pageRef.numberValue | 0; const numericIndex = pageRef.numberValue | 0;
if (numericIndex >= 0 && numericIndex < pages.length) { if (numericIndex >= 0 && numericIndex < pages.length)
return numericIndex; return numericIndex;
} }
if (pageRef.objectNumber !== undefined) {
const idxByObjNum = pages.findIndex(
(p) => p.ref.objectNumber === pageRef.objectNumber
);
if (idxByObjNum !== -1) return idxByObjNum;
}
if (pageRef.toString) {
const target = pageRef.toString();
const idxByString = pages.findIndex(
(p) => p.ref.toString() === target
);
if (idxByString !== -1) return idxByString;
}
if (resolved && resolved.get) {
for (let i = 0; i < pages.length; i++) {
const pageDict = doc.context.lookup(pages[i].ref);
if (pageDict === resolved) return i;
}
} }
} catch (e) { } catch (e) {
console.error('Error finding page index:', e); console.error('Error finding page:', e);
} }
// If we couldn't find a match, return 0 // Fallback: keep current behavior but log for diagnostics
console.warn('Falling back to page 0 for destination');
return 0; return 0;
} }
@@ -1988,24 +1959,7 @@ async function extractExistingBookmarks(doc) {
try { try {
const name = dest.decodeText ? dest.decodeText() : dest.toString(); const name = dest.decodeText ? dest.decodeText() : dest.toString();
if (namedDests.has(name)) { if (namedDests.has(name)) {
const namedDest = namedDests.get(name); dest = namedDests.get(name);
// Named destinations can be:
// 1. A direct array [pageRef, /XYZ, ...]
// 2. A dictionary with a 'D' entry containing the array
if (namedDest.array) {
dest = namedDest;
} else if (namedDest.lookup) {
// It's a dictionary - extract the 'D' entry
const destFromDict = namedDest.lookup(PDFName.of('D'));
if (destFromDict) {
dest = destFromDict;
} else {
dest = namedDest;
}
} else {
dest = namedDest;
}
} else if (dest.lookup) { } else if (dest.lookup) {
// Some named destinations resolve to a dictionary with 'D' entry // Some named destinations resolve to a dictionary with 'D' entry
const maybeDict = resolveRef(dest); const maybeDict = resolveRef(dest);
@@ -2020,9 +1974,7 @@ async function extractExistingBookmarks(doc) {
} }
} }
// dest is typically an array like [pageRef, /XYZ, x, y, zoom] return resolveRef(dest);
// Resolving it would corrupt the array structure
return dest;
} }
function traverse(item) { function traverse(item) {
@@ -2042,8 +1994,7 @@ async function extractExistingBookmarks(doc) {
if (dest && dest.array) { if (dest && dest.array) {
const pageRef = dest.array[0]; const pageRef = dest.array[0];
const resolvedPageRef = resolveRef(pageRef); pageIndex = findPageIndex(pageRef);
pageIndex = findPageIndex(pageRef, resolvedPageRef);
if (dest.array.length > 2) { if (dest.array.length > 2) {
const xObj = resolveRef(dest.array[2]); const xObj = resolveRef(dest.array[2]);
+26 -21
View File
@@ -3,14 +3,10 @@ import {
downloadFile, downloadFile,
hexToRgb, hexToRgb,
readFileAsArrayBuffer, readFileAsArrayBuffer,
getPDFDocument,
} from '../utils/helpers.js'; } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let isRenderingPreview = false; let isRenderingPreview = false;
let renderTimeout: any; let renderTimeout: any;
@@ -20,25 +16,29 @@ async function updateTextColorPreview() {
isRenderingPreview = true; isRenderingPreview = true;
try { try {
const textColorCanvas = document.getElementById('text-color-canvas') as HTMLCanvasElement; const textColorCanvas = document.getElementById('text-color-canvas');
if (!textColorCanvas) return; if (!textColorCanvas) return;
const pdf = await getPDFDocument( // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
const page = await pdf.getPage(1); // Preview first page const page = await pdf.getPage(1); // Preview first page
const viewport = page.getViewport({ scale: 0.8 }); const viewport = page.getViewport({ scale: 0.8 });
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
const context = textColorCanvas.getContext('2d'); const context = textColorCanvas.getContext('2d');
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
textColorCanvas.width = viewport.width; textColorCanvas.width = viewport.width;
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
textColorCanvas.height = viewport.height; textColorCanvas.height = viewport.height;
await page.render({ canvasContext: context, viewport, canvas: textColorCanvas }).promise; await page.render({ canvasContext: context, viewport }).promise;
const imageData = context.getImageData( const imageData = context.getImageData(
0, 0,
0, 0,
textColorCanvas.width, (textColorCanvas as HTMLCanvasElement).width,
textColorCanvas.height (textColorCanvas as HTMLCanvasElement).height
); );
const data = imageData.data; const data = imageData.data;
const colorHex = ( const colorHex = (
@@ -78,19 +78,21 @@ export async function setupTextColorTool() {
renderTimeout = setTimeout(updateTextColorPreview, 250); renderTimeout = setTimeout(updateTextColorPreview, 250);
}); });
const pdf = await getPDFDocument( // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
const page = await pdf.getPage(1); const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 0.8 }); const viewport = page.getViewport({ scale: 0.8 });
(originalCanvas as HTMLCanvasElement).width = viewport.width; // @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
(originalCanvas as HTMLCanvasElement).height = viewport.height; originalCanvas.width = viewport.width;
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
originalCanvas.height = viewport.height;
await page.render({ await page.render({
canvasContext: (originalCanvas as HTMLCanvasElement).getContext('2d'), canvasContext: (originalCanvas as HTMLCanvasElement).getContext('2d'),
viewport, viewport,
canvas: originalCanvas as HTMLCanvasElement,
}).promise; }).promise;
await updateTextColorPreview(); await updateTextColorPreview();
} }
@@ -101,14 +103,16 @@ export async function changeTextColor() {
return; return;
} }
const colorHex = (document.getElementById('text-color-input') as HTMLInputElement).value; // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color-input').value;
const { r, g, b } = hexToRgb(colorHex); const { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120; const darknessThreshold = 120;
showLoader('Changing text color...'); showLoader('Changing text color...');
try { try {
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const pdf = await getPDFDocument( // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
@@ -122,7 +126,7 @@ export async function changeTextColor() {
canvas.height = viewport.height; canvas.height = viewport.height;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport, canvas }).promise; await page.render({ canvasContext: context, viewport }).promise;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; const data = imageData.data;
@@ -140,15 +144,16 @@ export async function changeTextColor() {
} }
context.putImageData(imageData, 0, 0); context.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise<Uint8Array>((resolve) => const pngImageBytes = await new Promise((resolve) =>
canvas.toBlob((blob) => { canvas.toBlob((blob) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer)); // @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.readAsArrayBuffer(blob!); reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png') }, 'image/png')
); );
const pngImage = await newPdfDoc.embedPng(pngImageBytes); const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(pngImage, { newPage.drawImage(pngImage, {
x: 0, x: 0,
+16 -157
View File
@@ -1,209 +1,68 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib'; import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
document.addEventListener('change', (e) => {
const target = e.target as HTMLElement;
if (target.id === 'add-separator') {
const separatorOptions = document.getElementById('separator-options');
if (separatorOptions) {
const checkbox = target as HTMLInputElement;
if (checkbox.checked) {
separatorOptions.classList.remove('hidden');
} else {
separatorOptions.classList.add('hidden');
}
}
}
});
export async function combineToSinglePage() { export async function combineToSinglePage() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const orientation = document.getElementById('combine-orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const spacing = parseInt(document.getElementById('page-spacing').value) || 0; const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const backgroundColorHex = document.getElementById('background-color').value; const backgroundColorHex = document.getElementById('background-color').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addSeparator = document.getElementById('add-separator').checked; const addSeparator = document.getElementById('add-separator').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const separatorThickness = parseFloat(document.getElementById('separator-thickness').value) || 0.5;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const separatorColorHex = document.getElementById('separator-color').value;
const backgroundColor = hexToRgb(backgroundColorHex); const backgroundColor = hexToRgb(backgroundColorHex);
const separatorColor = hexToRgb(separatorColorHex);
showLoader('Combining pages...'); showLoader('Combining pages...');
try { try {
const sourceDoc = state.pdfDoc; const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create(); const newDoc = await PDFLibDocument.create();
const pdfBytes = await sourceDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
const sourcePages = sourceDoc.getPages(); const sourcePages = sourceDoc.getPages();
let maxWidth = 0;
let maxHeight = 0;
let totalWidth = 0;
let totalHeight = 0;
let maxWidth = 0;
let totalHeight = 0;
sourcePages.forEach((page: any) => { sourcePages.forEach((page: any) => {
const { width, height } = page.getSize(); const { width, height } = page.getSize();
if (width > maxWidth) maxWidth = width; if (width > maxWidth) maxWidth = width;
if (height > maxHeight) maxHeight = height;
totalWidth += width;
totalHeight += height; totalHeight += height;
}); });
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
let finalWidth, finalHeight; const newPage = newDoc.addPage([maxWidth, totalHeight]);
if (orientation === 'horizontal') {
finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing;
finalHeight = maxHeight;
} else {
finalWidth = maxWidth;
finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing;
}
const newPage = newDoc.addPage([finalWidth, finalHeight]);
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') { if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
newPage.drawRectangle({ newPage.drawRectangle({
x: 0, x: 0,
y: 0, y: 0,
width: finalWidth, width: maxWidth,
height: finalHeight, height: totalHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b), color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
}); });
} }
let currentX = 0; let currentY = totalHeight;
let currentY = finalHeight;
for (let i = 0; i < sourcePages.length; i++) { for (let i = 0; i < sourcePages.length; i++) {
const sourcePage = sourcePages[i]; const sourcePage = sourcePages[i];
const { width, height } = sourcePage.getSize(); const { width, height } = sourcePage.getSize();
const embeddedPage = await newDoc.embedPage(sourcePage);
try {
const page = await pdfjsDoc.getPage(i + 1);
const scale = 2.0;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
viewport,
canvas
}).promise;
const pngDataUrl = canvas.toDataURL('image/png');
const pngImage = await newDoc.embedPng(pngDataUrl);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
newPage.drawImage(pngImage, { x: currentX, y, width, height });
} else {
// Vertical layout: stack top to bottom
currentY -= height; currentY -= height;
const x = (finalWidth - width) / 2; // Center horizontally const x = (maxWidth - width) / 2;
newPage.drawImage(pngImage, { x, y: currentY, width, height });
}
} catch (renderError) {
console.warn(`Failed to render page ${i + 1} with PDF.js, trying fallback method:`, renderError);
// Fallback: try to copy and embed the page directly
try {
const [copiedPage] = await newDoc.copyPages(sourceDoc, [i]);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
const embeddedPage = await newDoc.embedPage(copiedPage);
newPage.drawPage(embeddedPage, { x: currentX, y, width, height });
} else {
currentY -= height;
const x = (finalWidth - width) / 2;
const embeddedPage = await newDoc.embedPage(copiedPage);
newPage.drawPage(embeddedPage, { x, y: currentY, width, height }); newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
}
} catch (embedError) {
console.error(`Failed to process page ${i + 1}:`, embedError);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
newPage.drawRectangle({
x: currentX,
y,
width,
height,
borderColor: rgb(0.8, 0, 0),
borderWidth: 2,
});
newPage.drawText(`Page ${i + 1} could not be rendered`, {
x: currentX + 10,
y: y + height / 2,
size: 12,
color: rgb(0.8, 0, 0),
});
} else {
currentY -= height;
const x = (finalWidth - width) / 2;
newPage.drawRectangle({
x,
y: currentY,
width,
height,
borderColor: rgb(0.8, 0, 0),
borderWidth: 2,
});
newPage.drawText(`Page ${i + 1} could not be rendered`, {
x: x + 10,
y: currentY + height / 2,
size: 12,
color: rgb(0.8, 0, 0),
});
}
}
}
// Draw separator line
if (addSeparator && i < sourcePages.length - 1) { if (addSeparator && i < sourcePages.length - 1) {
if (orientation === 'horizontal') {
const lineX = currentX + width + spacing / 2;
newPage.drawLine({
start: { x: lineX, y: 0 },
end: { x: lineX, y: finalHeight },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentX += width + spacing;
} else {
const lineY = currentY - spacing / 2; const lineY = currentY - spacing / 2;
newPage.drawLine({ newPage.drawLine({
start: { x: 0, y: lineY }, start: { x: 0, y: lineY },
end: { x: finalWidth, y: lineY }, end: { x: maxWidth, y: lineY },
thickness: separatorThickness, thickness: 0.5,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b), color: rgb(0.8, 0.8, 0.8),
}); });
}
currentY -= spacing; currentY -= spacing;
} }
} else {
if (orientation === 'horizontal') {
currentX += width + spacing;
} else {
currentY -= spacing;
}
}
}
const newPdfBytes = await newDoc.save(); const newPdfBytes = await newDoc.save();
downloadFile( downloadFile(
+3 -6
View File
@@ -1,10 +1,6 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const state = { const state = {
pdfDoc1: null, pdfDoc1: null,
@@ -126,7 +122,8 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
try { try {
showLoader(`Loading ${file.name}...`); showLoader(`Loading ${file.name}...`);
const pdfBytes = await readFileAsArrayBuffer(file); const pdfBytes = await readFileAsArrayBuffer(file);
state[docKey] = await getPDFDocument(pdfBytes).promise; // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
if (state.pdfDoc1 && state.pdfDoc2) { if (state.pdfDoc1 && state.pdfDoc2) {
document.getElementById('compare-viewer').classList.remove('hidden'); document.getElementById('compare-viewer').classList.remove('hidden');
+7 -4
View File
@@ -3,13 +3,11 @@ import {
downloadFile, downloadFile,
readFileAsArrayBuffer, readFileAsArrayBuffer,
formatBytes, formatBytes,
getPDFDocument,
} from '../utils/helpers.js'; } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
function dataUrlToBytes(dataUrl: any) { function dataUrlToBytes(dataUrl: any) {
const base64 = dataUrl.split(',')[1]; const base64 = dataUrl.split(',')[1];
@@ -185,7 +183,12 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
} }
async function performLegacyCompression(arrayBuffer: any, settings: any) { async function performLegacyCompression(arrayBuffer: any, settings: any) {
const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise; pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const newPdfDoc = await PDFDocument.create(); const newPdfDoc = await PDFDocument.create();
for (let i = 1; i <= pdfJsDoc.numPages; i++) { for (let i = 1; i <= pdfJsDoc.numPages; i++) {
+49 -47
View File
@@ -1,12 +1,10 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import Cropper from 'cropperjs'; import Cropper from 'cropperjs';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
// --- Global State for the Cropper Tool --- // --- Global State for the Cropper Tool ---
const cropperState = { const cropperState = {
pdfDoc: null, pdfDoc: null,
@@ -131,44 +129,45 @@ function enableControls() {
*/ */
async function performMetadataCrop(pdfToModify: any, cropData: any) { async function performMetadataCrop(pdfToModify: any, cropData: any) {
for (const pageNum in cropData) { for (const pageNum in cropData) {
const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
const viewport = pdfJsPage.getViewport({ scale: 1 });
const crop = cropData[pageNum];
// Man I hate doing math
// Calculate visual crop rectangle in viewport pixels
const cropX = viewport.width * crop.x;
const cropY = viewport.height * crop.y;
const cropW = viewport.width * crop.width;
const cropH = viewport.height * crop.height;
// Define the 4 corners of the crop rectangle in visual coordinates (Top-Left origin)
const visualCorners = [
{ x: cropX, y: cropY }, // TL
{ x: cropX + cropW, y: cropY }, // TR
{ x: cropX + cropW, y: cropY + cropH }, // BR
{ x: cropX, y: cropY + cropH }, // BL
];
// This handles rotation, media box offsets, and coordinate system flips automatically
const pdfCorners = visualCorners.map(p => {
return viewport.convertToPdfPoint(p.x, p.y);
});
// Find the bounding box of the converted points in PDF coordinates
// convertToPdfPoint returns [x, y] arrays
const pdfXs = pdfCorners.map(p => p[0]);
const pdfYs = pdfCorners.map(p => p[1]);
const minX = Math.min(...pdfXs);
const maxX = Math.max(...pdfXs);
const minY = Math.min(...pdfYs);
const maxY = Math.max(...pdfYs);
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message // @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
const page = pdfToModify.getPages()[pageNum - 1]; const page = pdfToModify.getPages()[pageNum - 1];
page.setCropBox(minX, minY, maxX - minX, maxY - minY); const { width: pageWidth, height: pageHeight } = page.getSize();
const rotation = page.getRotation().angle;
const crop = cropData[pageNum];
const visualPdfWidth = pageWidth * crop.width;
const visualPdfHeight = pageHeight * crop.height;
const visualPdfX = pageWidth * crop.x;
const visualPdfY = pageHeight * crop.y;
let finalX, finalY, finalWidth, finalHeight;
switch (rotation) {
case 90:
finalX = visualPdfY;
finalY = pageWidth - visualPdfX - visualPdfWidth;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
case 180:
finalX = pageWidth - visualPdfX - visualPdfWidth;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
case 270:
finalX = pageHeight - visualPdfY - visualPdfHeight;
finalY = visualPdfX;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
default:
finalX = visualPdfX;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
}
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
} }
} }
@@ -242,24 +241,24 @@ async function performFlatteningCrop(cropData: any) {
export async function setupCropperTool() { export async function setupCropperTool() {
if (state.files.length === 0) return; if (state.files.length === 0) return;
// Clear pageCrops on new file upload
try {
// Clear pageCrops on new file upload // Clear pageCrops on new file upload
cropperState.pageCrops = {}; cropperState.pageCrops = {};
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]); const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
cropperState.originalPdfBytes = arrayBuffer; cropperState.originalPdfBytes = arrayBuffer;
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0); const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
const loadingTask = getPDFDocument({ data: arrayBufferForPdfJs }); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
try {
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
cropperState.pdfDoc = await loadingTask.promise; cropperState.pdfDoc = await loadingTask.promise;
cropperState.currentPageNum = 1; cropperState.currentPageNum = 1;
await displayPageAsImage(cropperState.currentPageNum); await displayPageAsImage(cropperState.currentPageNum);
} catch (error) {
console.error('Error setting up cropper tool:', error);
showAlert('Error', 'Failed to load PDF for cropping.');
}
document document
.getElementById('prev-page') .getElementById('prev-page')
@@ -342,5 +341,8 @@ export async function setupCropperTool() {
hideLoader(); hideLoader();
} }
}); });
} catch (error) {
console.error('Error setting up cropper tool:', error);
showAlert('Error', 'Failed to load PDF for cropping.');
}
} }
+21 -62
View File
@@ -1,13 +1,9 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const duplicateOrganizeState = { const duplicateOrganizeState = {
sortableInstances: {}, sortableInstances: {},
@@ -86,22 +82,27 @@ export async function renderDuplicateOrganizeThumbnails() {
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
if (!grid) return; if (!grid) return;
// Cleanup any previous lazy loading observers
cleanupLazyRendering();
showLoader('Rendering page previews...'); showLoader('Rendering page previews...');
const pdfData = await state.pdfDoc.save(); const pdfData = await state.pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise; // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
grid.textContent = ''; grid.textContent = '';
// Function to create wrapper element for each page for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => { const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: canvas.getContext('2d'), viewport })
.promise;
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = wrapper.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-2'; 'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.originalPageIndex = pageNumber - 1; wrapper.dataset.originalPageIndex = i - 1;
const imgContainer = document.createElement('div'); const imgContainer = document.createElement('div');
imgContainer.className = imgContainer.className =
@@ -115,7 +116,7 @@ export async function renderDuplicateOrganizeThumbnails() {
const pageNumberSpan = document.createElement('span'); const pageNumberSpan = document.createElement('span');
pageNumberSpan.className = pageNumberSpan.className =
'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
pageNumberSpan.textContent = pageNumber.toString(); pageNumberSpan.textContent = i.toString();
const controlsDiv = document.createElement('div'); const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-4'; controlsDiv.className = 'flex items-center justify-center gap-4';
@@ -140,39 +141,14 @@ export async function renderDuplicateOrganizeThumbnails() {
controlsDiv.append(duplicateBtn, deleteBtn); controlsDiv.append(duplicateBtn, deleteBtn);
wrapper.append(imgContainer, pageNumberSpan, controlsDiv); wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
grid.appendChild(wrapper);
attachEventListeners(wrapper); attachEventListeners(wrapper);
return wrapper;
};
try {
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdfjsDoc,
grid,
createWrapper,
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '400px',
onProgress: (current, total) => {
showLoader(`Rendering page previews: ${current}/${total}`);
},
onBatchComplete: () => {
createIcons({ icons });
} }
}
);
initializePageGridSortable(); initializePageGridSortable();
} catch (error) { createIcons({ icons });
console.error('Error rendering thumbnails:', error);
showAlert('Error', 'Failed to render page previews');
} finally {
hideLoader(); hideLoader();
} }
}
export async function processAndSave() { export async function processAndSave() {
showLoader('Building new PDF...'); showLoader('Building new PDF...');
@@ -180,28 +156,11 @@ export async function processAndSave() {
const grid = document.getElementById('page-grid'); const grid = document.getElementById('page-grid');
const finalPageElements = grid.querySelectorAll('.page-thumbnail'); const finalPageElements = grid.querySelectorAll('.page-thumbnail');
const finalIndices = Array.from(finalPageElements) const finalIndices = Array.from(finalPageElements).map((el) =>
.map((el) => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10)) parseInt((el as HTMLElement).dataset.originalPageIndex)
.filter(index => !isNaN(index) && index >= 0); );
console.log('Saving PDF with indices:', finalIndices);
console.log('Original PDF Page Count:', state.pdfDoc?.getPageCount());
if (finalIndices.length === 0) {
showAlert('Error', 'No valid pages to save.');
return;
}
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const totalPages = state.pdfDoc.getPageCount();
const invalidIndices = finalIndices.filter(i => i >= totalPages);
if (invalidIndices.length > 0) {
console.error('Found invalid indices:', invalidIndices);
showAlert('Error', 'Some pages could not be processed. Please try again.');
return;
}
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices); const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
copiedPages.forEach((page: any) => newPdfDoc.addPage(page)); copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
@@ -211,8 +170,8 @@ export async function processAndSave() {
'organized.pdf' 'organized.pdf'
); );
} catch (e) { } catch (e) {
console.error('Save error:', e); console.error(e);
showAlert('Error', 'Failed to save the new PDF. Check console for details.'); showAlert('Error', 'Failed to save the new PDF.');
} finally { } finally {
hideLoader(); hideLoader();
} }
+5 -60
View File
@@ -1,6 +1,5 @@
import { downloadFile, formatBytes } from '../utils/helpers.js'; import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { showAlert } from '../ui.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
const worker = new Worker('/workers/extract-attachments.worker.js'); const worker = new Worker('/workers/extract-attachments.worker.js');
@@ -67,30 +66,6 @@ worker.onmessage = (e: MessageEvent<ExtractAttachmentResponse>) => {
if (e.data.status === 'success') { if (e.data.status === 'success') {
const attachments = e.data.attachments; const attachments = e.data.attachments;
if (attachments.length === 0) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
state.files = [];
state.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.add('hidden');
}
return;
}
const zip = new JSZip(); const zip = new JSZip();
let totalSize = 0; let totalSize = 0;
@@ -101,60 +76,29 @@ worker.onmessage = (e: MessageEvent<ExtractAttachmentResponse>) => {
zip.generateAsync({ type: 'blob' }).then((zipBlob) => { zip.generateAsync({ type: 'blob' }).then((zipBlob) => {
downloadFile(zipBlob, 'extracted-attachments.zip'); downloadFile(zipBlob, 'extracted-attachments.zip');
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
showStatus( showStatus(
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`, `Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
'success' 'success'
); );
state.files = []; state.files = [];
state.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area'); const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) { if (fileDisplayArea) {
fileDisplayArea.innerHTML = ''; fileDisplayArea.innerHTML = '';
fileDisplayArea.classList.add('hidden');
} }
const fileInput = document.getElementById('file-input') as HTMLInputElement; const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) { if (fileInput) {
fileInput.value = ''; fileInput.value = '';
} }
document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
const fileControls = document.getElementById('file-controls'); document.getElementById('process-btn')?.setAttribute('disabled', 'true');
if (fileControls) {
fileControls.classList.add('hidden');
}
}); });
} else if (e.data.status === 'error') { } else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.'; const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage); console.error('Worker Error:', errorMessage);
if (errorMessage.includes('No attachments were found')) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
state.files = [];
state.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.add('hidden');
}
} else {
showStatus(`Error: ${errorMessage}`, 'error'); showStatus(`Error: ${errorMessage}`, 'error');
} }
}
}; };
worker.onerror = (error) => { worker.onerror = (error) => {
@@ -169,7 +113,8 @@ function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info'
if (!statusMessage) return; if (!statusMessage) return;
statusMessage.textContent = message; statusMessage.textContent = message;
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success' statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
type === 'success'
? 'bg-green-900 text-green-200' ? 'bg-green-900 text-green-200'
: type === 'error' : type === 'error'
? 'bg-red-900 text-red-200' ? 'bg-red-900 text-red-200'
+16 -65
View File
@@ -1,7 +1,6 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument } from 'pdf-lib';
export function flattenFormsInDoc(pdfDoc) { export function flattenFormsInDoc(pdfDoc) {
const form = pdfDoc.getForm(); const form = pdfDoc.getForm();
@@ -9,78 +8,30 @@ export function flattenFormsInDoc(pdfDoc) {
} }
export async function flatten() { export async function flatten() {
if (state.files.length === 0) { if (!state.pdfDoc) {
showAlert('No Files', 'Please select at least one PDF file.'); showAlert('Error', 'PDF not loaded.');
return; return;
} }
try {
if (state.files.length === 1) {
showLoader('Flattening PDF...'); showLoader('Flattening PDF...');
const file = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
try { try {
flattenFormsInDoc(pdfDoc); flattenFormsInDoc(state.pdfDoc);
} catch (e) {
if (e.message.includes('getForm')) {
// Ignore if no form found
} else {
throw e;
}
}
const flattenedBytes = await pdfDoc.save(); const flattenedBytes = await state.pdfDoc.save();
downloadFile( downloadFile(
new Blob([flattenedBytes as any], { type: 'application/pdf' }), new Blob([flattenedBytes], { type: 'application/pdf' }),
`flattened_${file.name}` 'flattened.pdf'
); );
hideLoader();
} else {
showLoader('Flattening multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
let processedCount = 0;
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Flattening ${i + 1}/${state.files.length}: ${file.name}...`);
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
try {
flattenFormsInDoc(pdfDoc);
} catch (e) {
if (e.message.includes('getForm')) {
// Ignore if no form found
} else {
throw e;
}
}
const flattenedBytes = await pdfDoc.save();
zip.file(`flattened_${file.name}`, flattenedBytes);
processedCount++;
} catch (e) {
console.error(`Error processing ${file.name}:`, e);
}
}
if (processedCount > 0) {
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'flattened_pdfs.zip');
showAlert('Success', `Processed ${processedCount} PDFs.`);
} else {
showAlert('Error', 'No PDFs could be processed.');
}
hideLoader();
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
if (e.message.includes('getForm')) {
showAlert(
'No Form Found',
'This PDF does not contain any form fields to flatten.'
);
} else {
showAlert('Error', 'Could not flatten the PDF.');
}
} finally {
hideLoader(); hideLoader();
showAlert('Error', e.message || 'An unexpected error occurred.');
} }
} }
File diff suppressed because it is too large Load Diff
+4 -7
View File
@@ -1,11 +1,7 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function invertColors() { export async function invertColors() {
if (!state.pdfDoc) { if (!state.pdfDoc) {
@@ -16,7 +12,8 @@ export async function invertColors() {
try { try {
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save(); const pdfBytes = await state.pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise; // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) { for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i); const page = await pdfjsDoc.getPage(i);
@@ -25,7 +22,7 @@ export async function invertColors() {
canvas.width = viewport.width; canvas.width = viewport.width;
canvas.height = viewport.height; canvas.height = viewport.height;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport, canvas }).promise; await page.render({ canvasContext: ctx, viewport }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; const data = imageData.data;
+1 -2
View File
@@ -1,6 +1,5 @@
import JSZip from 'jszip' import JSZip from 'jszip'
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers'; import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
const worker = new Worker(new URL('/workers/json-to-pdf.worker.js', import.meta.url)); const worker = new Worker(new URL('/workers/json-to-pdf.worker.js', import.meta.url));
@@ -156,4 +155,4 @@ convertBtn.addEventListener('click', convertJSONsToPDF)
// Initialize // Initialize
showStatus('Select JSON files to get started', 'info') showStatus('Select JSON files to get started', 'info')
initializeGlobalShortcuts()
+103 -171
View File
@@ -1,17 +1,18 @@
import { showLoader, hideLoader, showAlert } from '../ui.ts'; import { showLoader, hideLoader, showAlert } from '../ui.ts';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.ts'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts';
import { state } from '../state.ts'; import { state } from '../state.ts';
import { renderPagesProgressively, cleanupLazyRendering, createPlaceholder } from '../utils/render-utils.ts';
import { createIcons, icons } from 'lucide'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const mergeState = { const mergeState = {
pdfDocs: {} as Record<string, any>, pdfDocs: {},
pdfBytes: {} as Record<string, ArrayBuffer>,
activeMode: 'file', activeMode: 'file',
sortableInstances: {}, sortableInstances: {},
isRendering: false, isRendering: false,
@@ -19,8 +20,6 @@ const mergeState = {
lastFileHash: null, lastFileHash: null,
}; };
const mergeWorker = new Worker('/workers/merge.worker.js');
function parsePageRanges(rangeString: any, totalPages: any) { function parsePageRanges(rangeString: any, totalPages: any) {
const indices = new Set(); const indices = new Set();
if (!rangeString.trim()) return []; if (!rangeString.trim()) return [];
@@ -130,24 +129,44 @@ async function renderPageMergeThumbnails() {
mergeState.isRendering = true; mergeState.isRendering = true;
container.textContent = ''; container.textContent = '';
cleanupLazyRendering(); let currentPageNumber = 0;
let totalPages = state.files.reduce((sum, file) => {
let totalPages = 0; const pdfDoc = mergeState.pdfDocs[file.name];
for (const file of state.files) { return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
const doc = mergeState.pdfDocs[file.name]; }, 0);
if (doc) totalPages += doc.numPages;
}
try { try {
let currentPageNumber = 0; const thumbnailsHTML = [];
for (const file of state.files) {
const pdfDoc = mergeState.pdfDocs[file.name];
if (!pdfDoc) continue;
const pdfData = await pdfDoc.save();
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
currentPageNumber++;
showLoader(
`Rendering page previews: ${currentPageNumber}/${totalPages}`
);
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.3 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
canvas: canvas,
viewport,
}).promise;
// Function to create wrapper element for each page
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = wrapper.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors'; 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
wrapper.dataset.fileName = fileName || ''; wrapper.dataset.fileName = file.name;
wrapper.dataset.pageIndex = (pageNumber - 1).toString(); wrapper.dataset.pageIndex = (i - 1).toString();
const imgContainer = document.createElement('div'); const imgContainer = document.createElement('div');
imgContainer.className = 'relative'; imgContainer.className = 'relative';
@@ -159,53 +178,22 @@ async function renderPageMergeThumbnails() {
const pageNumDiv = document.createElement('div'); const pageNumDiv = document.createElement('div');
pageNumDiv.className = pageNumDiv.className =
'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg'; 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
pageNumDiv.textContent = pageNumber.toString(); pageNumDiv.textContent = i.toString();
imgContainer.append(img, pageNumDiv); imgContainer.append(img, pageNumDiv);
const fileNamePara = document.createElement('p'); const fileNamePara = document.createElement('p');
fileNamePara.className = fileNamePara.className =
'text-xs text-gray-400 truncate w-full text-center'; 'text-xs text-gray-400 truncate w-full text-center';
const fullTitle = fileName ? `${fileName} (page ${pageNumber})` : `Page ${pageNumber}`; const fullTitle = `${file.name} (page ${i})`;
fileNamePara.title = fullTitle; fileNamePara.title = fullTitle;
fileNamePara.textContent = fileName fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
? `${fileName.substring(0, 10)}... (p${pageNumber})`
: `Page ${pageNumber}`;
wrapper.append(imgContainer, fileNamePara); wrapper.append(imgContainer, fileNamePara);
return wrapper; container.appendChild(wrapper);
};
// Render pages from all files progressively
for (const file of state.files) {
const pdfjsDoc = mergeState.pdfDocs[file.name];
if (!pdfjsDoc) continue;
// Create a wrapper function that includes the file name
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {
return createWrapper(canvas, pageNumber, file.name);
};
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdfjsDoc,
container,
createWrapperWithFileName,
{
batchSize: 6,
useLazyLoading: true,
lazyLoadMargin: '300px',
onProgress: (current, total) => {
currentPageNumber++;
showLoader(
`Rendering page previews...`
);
},
onBatchComplete: () => {
createIcons({ icons });
} }
}
); pdfjsDoc.destroy();
} }
mergeState.cachedThumbnails = true; mergeState.cachedThumbnails = true;
@@ -224,159 +212,91 @@ async function renderPageMergeThumbnails() {
export async function merge() { export async function merge() {
showLoader('Merging PDFs...'); showLoader('Merging PDFs...');
try { try {
const jobs: any[] = []; const newPdfDoc = await PDFLibDocument.create();
const filesToMerge: any[] = [];
const uniqueFileNames = new Set<string>();
if (mergeState.activeMode === 'file') { if (mergeState.activeMode === 'file') {
const fileList = document.getElementById('file-list'); const fileList = document.getElementById('file-list');
if (!fileList) throw new Error('File list not found');
const sortedFiles = Array.from(fileList.children) const sortedFiles = Array.from(fileList.children)
.map((li) => { .map((li) => {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
return state.files.find((f) => f.name === li.dataset.fileName); return state.files.find((f) => f.name === li.dataset.fileName);
}) })
.filter(Boolean); .filter(Boolean);
for (const file of sortedFiles) { for (const file of sortedFiles) {
if (!file) continue;
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_'); const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
const rangeInput = document.getElementById(`range-${safeFileName}`) as HTMLInputElement; const rangeInput = document.getElementById(`range-${safeFileName}`);
if (!rangeInput) continue;
uniqueFileNames.add(file.name); // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const rangeInputValue = rangeInput.value;
const sourcePdf = mergeState.pdfDocs[file.name];
if (!sourcePdf) continue;
if (rangeInput && rangeInput.value.trim()) { const totalPages = sourcePdf.getPageCount();
jobs.push({ const pageIndices = parsePageRanges(rangeInputValue, totalPages);
fileName: file.name,
rangeType: 'specific', const indicesToCopy =
rangeString: rangeInput.value.trim() pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
}); const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
} else { copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
jobs.push({
fileName: file.name,
rangeType: 'all'
});
}
} }
} else { } else {
// Page Mode
const pageContainer = document.getElementById('page-merge-preview'); const pageContainer = document.getElementById('page-merge-preview');
if (!pageContainer) throw new Error('Page container not found');
const pageElements = Array.from(pageContainer.children); const pageElements = Array.from(pageContainer.children);
const rawPages: { fileName: string; pageIndex: number }[] = [];
for (const el of pageElements) { for (const el of pageElements) {
// @ts-expect-error TS(2339) // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const fileName = el.dataset.fileName; const fileName = el.dataset.fileName;
// @ts-expect-error TS(2339) // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
const pageIndex = parseInt(el.dataset.pageIndex, 10); // 0-based index from dataset const pageIndex = parseInt(el.dataset.pageIndex, 10);
if (fileName && !isNaN(pageIndex)) { const sourcePdf = mergeState.pdfDocs[fileName];
uniqueFileNames.add(fileName); if (sourcePdf && !isNaN(pageIndex)) {
rawPages.push({ fileName, pageIndex }); const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [
} pageIndex,
} ]);
newPdfDoc.addPage(copiedPage);
// Group contiguous pages
for (let i = 0; i < rawPages.length; i++) {
const current = rawPages[i];
let endPage = current.pageIndex;
while (
i + 1 < rawPages.length &&
rawPages[i + 1].fileName === current.fileName &&
rawPages[i + 1].pageIndex === endPage + 1
) {
endPage++;
i++;
}
if (endPage === current.pageIndex) {
// Single page
jobs.push({
fileName: current.fileName,
rangeType: 'single',
pageIndex: current.pageIndex
});
} else {
// Range of pages
jobs.push({
fileName: current.fileName,
rangeType: 'range',
startPage: current.pageIndex + 1,
endPage: endPage + 1
});
} }
} }
} }
if (jobs.length === 0) { const mergedPdfBytes = await newPdfDoc.save();
showAlert('Error', 'No files or pages selected to merge.'); downloadFile(
hideLoader(); new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
return; 'merged.pdf'
} );
for (const name of uniqueFileNames) {
const bytes = mergeState.pdfBytes[name];
if (bytes) {
filesToMerge.push({ name, data: bytes });
}
}
mergeWorker.postMessage({
command: 'merge',
files: filesToMerge,
jobs: jobs
}, filesToMerge.map(f => f.data));
mergeWorker.onmessage = (e) => {
hideLoader();
if (e.data.status === 'success') {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
downloadFile(blob, 'merged.pdf');
showAlert('Success', 'PDFs merged successfully!'); showAlert('Success', 'PDFs merged successfully!');
} else {
console.error('Worker merge error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to merge PDFs.');
}
};
mergeWorker.onerror = (e) => {
hideLoader();
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) { } catch (e) {
console.error('Merge error:', e); console.error('Merge error:', e);
showAlert( showAlert(
'Error', 'Error',
'Failed to merge PDFs. Please check that all files are valid and not password-protected.' 'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
); );
} finally {
hideLoader(); hideLoader();
} }
} }
export async function setupMergeTool() { export async function setupMergeTool() {
document.getElementById('merge-options')?.classList.remove('hidden'); document.getElementById('merge-options').classList.remove('hidden');
const processBtn = document.getElementById('process-btn') as HTMLButtonElement; // @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
if (processBtn) processBtn.disabled = false; document.getElementById('process-btn').disabled = false;
const wasInPageMode = mergeState.activeMode === 'page'; const wasInPageMode = mergeState.activeMode === 'page';
showLoader('Loading PDF documents...'); showLoader('Loading PDF documents...');
try { try {
mergeState.pdfDocs = {};
mergeState.pdfBytes = {};
for (const file of state.files) { for (const file of state.files) {
if (!mergeState.pdfDocs[file.name]) {
const pdfBytes = await readFileAsArrayBuffer(file); const pdfBytes = await readFileAsArrayBuffer(file);
mergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer; mergeState.pdfDocs[file.name] = await PDFLibDocument.load(
pdfBytes as ArrayBuffer,
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0); {
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise; ignoreEncryption: true,
mergeState.pdfDocs[file.name] = pdfjsDoc; }
);
}
} }
} catch (error) { } catch (error) {
console.error('Error loading PDFs:', error); console.error('Error loading PDFs:', error);
@@ -392,12 +312,10 @@ export async function setupMergeTool() {
const pagePanel = document.getElementById('page-mode-panel'); const pagePanel = document.getElementById('page-mode-panel');
const fileList = document.getElementById('file-list'); const fileList = document.getElementById('file-list');
if (!fileModeBtn || !pageModeBtn || !filePanel || !pagePanel || !fileList) return;
fileList.textContent = ''; // Clear list safely fileList.textContent = ''; // Clear list safely
(state.files as File[]).forEach((f) => { (state.files as File[]).forEach((f) => {
const doc = mergeState.pdfDocs[f.name]; const doc = mergeState.pdfDocs[f.name];
const pageCount = doc ? doc.numPages : 'N/A'; const pageCount = doc ? doc.getPageCount() : 'N/A';
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_'); const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
const li = document.createElement('li'); const li = document.createElement('li');
@@ -442,8 +360,8 @@ export async function setupMergeTool() {
initializeFileListSortable(); initializeFileListSortable();
const newFileModeBtn = fileModeBtn.cloneNode(true) as HTMLElement; const newFileModeBtn = fileModeBtn.cloneNode(true);
const newPageModeBtn = pageModeBtn.cloneNode(true) as HTMLElement; const newPageModeBtn = pageModeBtn.cloneNode(true);
fileModeBtn.replaceWith(newFileModeBtn); fileModeBtn.replaceWith(newFileModeBtn);
pageModeBtn.replaceWith(newPageModeBtn); pageModeBtn.replaceWith(newPageModeBtn);
@@ -454,9 +372,13 @@ export async function setupMergeTool() {
filePanel.classList.remove('hidden'); filePanel.classList.remove('hidden');
pagePanel.classList.add('hidden'); pagePanel.classList.add('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-indigo-600', 'text-white'); newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white'); newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300'); newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
}); });
@@ -467,9 +389,13 @@ export async function setupMergeTool() {
filePanel.classList.add('hidden'); filePanel.classList.add('hidden');
pagePanel.classList.remove('hidden'); pagePanel.classList.remove('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-indigo-600', 'text-white'); newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white'); newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300'); newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
await renderPageMergeThumbnails(); await renderPageMergeThumbnails();
@@ -480,14 +406,20 @@ export async function setupMergeTool() {
filePanel.classList.add('hidden'); filePanel.classList.add('hidden');
pagePanel.classList.remove('hidden'); pagePanel.classList.remove('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-indigo-600', 'text-white'); newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white'); newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300'); newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
await renderPageMergeThumbnails(); await renderPageMergeThumbnails();
} else { } else {
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-indigo-600', 'text-white'); newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300'); newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
} }
} }
+4 -7
View File
@@ -1,14 +1,10 @@
import { tesseractLanguages } from '../config/tesseract-languages.js'; import { tesseractLanguages } from '../config/tesseract-languages.js';
import { showAlert } from '../ui.js'; import { showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import Tesseract from 'tesseract.js'; import Tesseract from 'tesseract.js';
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib'; import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let searchablePdfBytes: any = null; let searchablePdfBytes: any = null;
@@ -121,7 +117,8 @@ async function runOCR() {
tessedit_char_whitelist: whitelist, tessedit_char_whitelist: whitelist,
}); });
const pdf = await getPDFDocument( // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
@@ -139,7 +136,7 @@ async function runOCR() {
canvas.width = viewport.width; canvas.width = viewport.width;
canvas.height = viewport.height; canvas.height = viewport.height;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport, canvas }).promise; await page.render({ canvasContext: context, viewport }).promise;
if (binarize) { if (binarize) {
binarizeCanvas(context); binarizeCanvas(context);
+4 -182
View File
@@ -1,120 +1,8 @@
import { state } from '../state.js'; import { state } from '../state.js';
import { getStandardPageName, convertPoints } from '../utils/helpers.js'; import { getStandardPageName, convertPoints } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing
function calculateAspectRatio(width: number, height: number): string {
const ratio = width / height;
return ratio.toFixed(3);
}
function calculateArea(width: number, height: number, unit: string): string {
const areaInPoints = width * height;
let convertedArea = 0;
let unitSuffix = '';
switch (unit) {
case 'in':
convertedArea = areaInPoints / (72 * 72); // 72 points per inch
unitSuffix = 'in²';
break;
case 'mm':
convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4); // Convert to mm²
unitSuffix = 'mm²';
break;
case 'px':
const pxPerPoint = 96 / 72;
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
unitSuffix = 'px²';
break;
default: // 'pt'
convertedArea = areaInPoints;
unitSuffix = 'pt²';
break;
}
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
}
function getSummaryStats() {
const totalPages = analyzedPagesData.length;
// Count unique page sizes
const uniqueSizes = new Map();
analyzedPagesData.forEach((pageData: any) => {
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
const label = `${pageData.standardSize} (${pageData.orientation})`;
uniqueSizes.set(key, {
count: (uniqueSizes.get(key)?.count || 0) + 1,
label: label,
width: pageData.width,
height: pageData.height
});
});
const hasMixedSizes = uniqueSizes.size > 1;
return {
totalPages,
uniqueSizesCount: uniqueSizes.size,
uniqueSizes: Array.from(uniqueSizes.values()),
hasMixedSizes
};
}
function renderSummary() {
const summaryContainer = document.getElementById('dimensions-summary');
if (!summaryContainer) return;
const stats = getSummaryStats();
let summaryHTML = `
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Total Pages</p>
<p class="text-2xl font-bold text-white">${stats.totalPages}</p>
</div>
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Unique Page Sizes</p>
<p class="text-2xl font-bold text-white">${stats.uniqueSizesCount}</p>
</div>
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Document Type</p>
<p class="text-2xl font-bold ${stats.hasMixedSizes ? 'text-yellow-400' : 'text-green-400'}">
${stats.hasMixedSizes ? 'Mixed Sizes' : 'Uniform'}
</p>
</div>
</div>
`;
if (stats.hasMixedSizes) {
summaryHTML += `
<div class="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-4 mb-4">
<div class="flex items-start gap-3">
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0"></i>
<div>
<h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4>
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
<ul class="space-y-1 text-sm text-gray-300">
${stats.uniqueSizes.map((size: any) => `
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
`).join('')}
</ul>
</div>
</div>
</div>
`;
}
summaryContainer.innerHTML = summaryHTML;
if (stats.hasMixedSizes) {
createIcons({ icons });
}
}
/** /**
* Renders the dimensions table based on the stored data and selected unit. * Renders the dimensions table based on the stored data and selected unit.
* @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px'). * @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px').
@@ -128,89 +16,31 @@ function renderTable(unit: any) {
analyzedPagesData.forEach((pageData) => { analyzedPagesData.forEach((pageData) => {
const width = convertPoints(pageData.width, unit); const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit); const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = document.createElement('tr'); const row = document.createElement('tr');
// Page number // Create and append each cell safely using textContent
const pageNumCell = document.createElement('td'); const pageNumCell = document.createElement('td');
pageNumCell.className = 'px-4 py-3 text-white'; pageNumCell.className = 'px-4 py-3 text-white';
pageNumCell.textContent = pageData.pageNum; pageNumCell.textContent = pageData.pageNum;
// Dimensions
const dimensionsCell = document.createElement('td'); const dimensionsCell = document.createElement('td');
dimensionsCell.className = 'px-4 py-3 text-gray-300'; dimensionsCell.className = 'px-4 py-3 text-gray-300';
dimensionsCell.textContent = `${width} x ${height} ${unit}`; dimensionsCell.textContent = `${width} x ${height} ${unit}`;
// Standard size
const sizeCell = document.createElement('td'); const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-3 text-gray-300'; sizeCell.className = 'px-4 py-3 text-gray-300';
sizeCell.textContent = pageData.standardSize; sizeCell.textContent = pageData.standardSize;
// Orientation
const orientationCell = document.createElement('td'); const orientationCell = document.createElement('td');
orientationCell.className = 'px-4 py-3 text-gray-300'; orientationCell.className = 'px-4 py-3 text-gray-300';
orientationCell.textContent = pageData.orientation; orientationCell.textContent = pageData.orientation;
// Aspect Ratio row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
const aspectRatioCell = document.createElement('td');
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
aspectRatioCell.textContent = aspectRatio;
// Area
const areaCell = document.createElement('td');
areaCell.className = 'px-4 py-3 text-gray-300';
areaCell.textContent = area;
// Rotation
const rotationCell = document.createElement('td');
rotationCell.className = 'px-4 py-3 text-gray-300';
rotationCell.textContent = `${pageData.rotation}°`;
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell);
tableBody.appendChild(row); tableBody.appendChild(row);
}); });
} }
function exportToCSV() {
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
const unit = unitsSelect?.value || 'pt';
const headers = ['Page #', `Width (${unit})`, `Height (${unit})`, 'Standard Size', 'Orientation', 'Aspect Ratio', `Area (${unit}²)`, 'Rotation'];
const csvRows = [headers.join(',')];
analyzedPagesData.forEach((pageData: any) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = [
pageData.pageNum,
width,
height,
pageData.standardSize,
pageData.orientation,
aspectRatio,
area,
`${pageData.rotation}°`
];
csvRows.push(row.join(','));
});
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'page-dimensions.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/** /**
* Main function to analyze the PDF and display dimensions. * Main function to analyze the PDF and display dimensions.
* This is called once after the file is loaded. * This is called once after the file is loaded.
@@ -223,36 +53,28 @@ export function analyzeAndDisplayDimensions() {
pages.forEach((page: any, index: any) => { pages.forEach((page: any, index: any) => {
const { width, height } = page.getSize(); const { width, height } = page.getSize();
const rotation = page.getRotation().angle || 0;
analyzedPagesData.push({ analyzedPagesData.push({
pageNum: index + 1, pageNum: index + 1,
width, // Store raw width in points width, // Store raw width in points
height, // Store raw height in points height, // Store raw height in points
orientation: width > height ? 'Landscape' : 'Portrait', orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height), standardSize: getStandardPageName(width, height),
rotation: rotation
}); });
}); });
const resultsContainer = document.getElementById('dimensions-results'); const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById('units-select'); const unitsSelect = document.getElementById('units-select');
renderSummary();
// Initial render with default unit (points) // Initial render with default unit (points)
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
renderTable(unitsSelect.value); renderTable(unitsSelect.value);
// Show the results table
resultsContainer.classList.remove('hidden'); resultsContainer.classList.remove('hidden');
// Add event listener to handle unit changes
unitsSelect.addEventListener('change', (e) => { unitsSelect.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
renderTable(e.target.value); renderTable(e.target.value);
}); });
const exportButton = document.getElementById('export-csv-btn');
if (exportButton) {
exportButton.addEventListener('click', exportToCSV);
}
} }
+107 -383
View File
@@ -1,11 +1,12 @@
// @TODO:@ALAM- sometimes I think... and then I forget...
//
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib'; import { degrees, PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import JSZip from 'jszip'; import JSZip from 'jszip';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { downloadFile, getPDFDocument } from '../utils/helpers'; import { downloadFile } from '../utils/helpers';
import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -13,19 +14,13 @@ pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
).toString(); ).toString();
interface PageData { interface PageData {
id: string; // Unique ID for DOM reconciliation
pdfIndex: number; pdfIndex: number;
pageIndex: number; pageIndex: number;
rotation: number; rotation: number;
visualRotation: number; visualRotation: number;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement;
pdfDoc: PDFLibDocument; pdfDoc: PDFLibDocument;
originalPageIndex: number; originalPageIndex: number;
fileName: string; // Added for lazy loading identification
}
function generateId(): string {
return Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
} }
let allPages: PageData[] = []; let allPages: PageData[] = [];
@@ -95,8 +90,6 @@ function hideModal() {
} }
function showLoading(current: number, total: number) { function showLoading(current: number, total: number) {
// renderPagesProgressively handles loading UI via onProgress callback
// but we can keep this for compatibility if needed
const loader = document.getElementById('loading-overlay'); const loader = document.getElementById('loading-overlay');
const progress = document.getElementById('loading-progress'); const progress = document.getElementById('loading-progress');
const text = document.getElementById('loading-text'); const text = document.getElementById('loading-text');
@@ -106,27 +99,7 @@ function showLoading(current: number, total: number) {
loader.classList.remove('hidden'); loader.classList.remove('hidden');
const percentage = Math.round((current / total) * 100); const percentage = Math.round((current / total) * 100);
progress.style.width = `${percentage}%`; progress.style.width = `${percentage}%`;
text.textContent = `Rendering pages...`; text.textContent = `Rendering pages... ${current} of ${total}`;
}
async function withButtonLoading(buttonId: string, action: () => Promise<void>) {
const button = document.getElementById(buttonId) as HTMLButtonElement;
if (!button) return;
const originalContent = button.innerHTML;
const originalPointerEvents = button.style.pointerEvents;
try {
button.disabled = true;
button.style.pointerEvents = 'none';
button.innerHTML = '<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>';
await action();
} finally {
button.disabled = false;
button.style.pointerEvents = originalPointerEvents;
button.innerHTML = originalContent;
}
} }
function hideLoading() { function hideLoading() {
@@ -134,28 +107,18 @@ function hideLoading() {
if (loader) loader.classList.add('hidden'); if (loader) loader.classList.add('hidden');
} }
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('PDF Multi Tool: DOMContentLoaded');
initializeTool(); initializeTool();
}); });
} else {
console.log('PDF Multi Tool: DOMContentLoaded already fired, initializing immediately');
initializeTool();
}
function initializeTool() { function initializeTool() {
console.log('PDF Multi Tool: Initializing...');
createIcons({ icons }); createIcons({ icons });
initializeGlobalShortcuts();
document.getElementById('close-tool-btn')?.addEventListener('click', () => { document.getElementById('close-tool-btn')?.addEventListener('click', () => {
window.location.href = '/'; window.location.href = '/';
}); });
document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => { document.getElementById('upload-pdfs-btn')?.addEventListener('click', () => {
console.log('Upload button clicked, isRendering:', isRendering);
if (isRendering) { if (isRendering) {
showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info'); showModal('Please Wait', 'Pages are still being rendered. Please wait...', 'info');
return; return;
@@ -193,36 +156,19 @@ function initializeTool() {
}); });
document.getElementById('bulk-download-btn')?.addEventListener('click', () => { document.getElementById('bulk-download-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
if (selectedPages.size === 0) { bulkDownload();
showModal('No Pages Selected', 'Please select at least one page to download.', 'info');
return;
}
withButtonLoading('bulk-download-btn', async () => {
await downloadPagesAsPdf(Array.from(selectedPages).sort((a, b) => a - b), 'selected-pages.pdf');
}); });
});
document.getElementById('select-all-btn')?.addEventListener('click', () => { document.getElementById('select-all-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
snapshot();
selectAll(); selectAll();
}); });
document.getElementById('deselect-all-btn')?.addEventListener('click', () => { document.getElementById('deselect-all-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
snapshot();
deselectAll(); deselectAll();
}); });
document.getElementById('export-pdf-btn')?.addEventListener('click', () => { document.getElementById('export-pdf-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
if (allPages.length === 0) { downloadAll();
showModal('No Pages', 'There are no pages to export.', 'info');
return;
}
withButtonLoading('export-pdf-btn', async () => {
await downloadAll();
});
}); });
document.getElementById('add-blank-page-btn')?.addEventListener('click', () => { document.getElementById('add-blank-page-btn')?.addEventListener('click', () => {
if (isRendering) return; if (isRendering) return;
@@ -294,25 +240,21 @@ function initializeTool() {
} }
function resetAll() { function resetAll() {
renderCancelled = true;
isRendering = false;
snapshot(); snapshot();
allPages = []; allPages = [];
selectedPages.clear(); selectedPages.clear();
splitMarkers.clear(); splitMarkers.clear();
currentPdfDocs = []; currentPdfDocs = [];
pageCanvasCache.clear(); pageCanvasCache.clear();
cleanupLazyRendering(); renderCancelled = false;
isRendering = false;
// Destroy sortable instance
if (sortableInstance) { if (sortableInstance) {
sortableInstance.destroy(); sortableInstance.destroy();
sortableInstance = null; sortableInstance = null;
} }
// Force clear DOM to prevent ghost pages
const pagesContainer = document.getElementById('pages-container');
if (pagesContainer) pagesContainer.innerHTML = '';
updatePageDisplay(); updatePageDisplay();
document.getElementById('upload-area')?.classList.remove('hidden'); document.getElementById('upload-area')?.classList.remove('hidden');
} }
@@ -334,80 +276,43 @@ async function loadPdfs(files: File[]) {
const uploadArea = document.getElementById('upload-area'); const uploadArea = document.getElementById('upload-area');
if (uploadArea) uploadArea.classList.add('hidden'); if (uploadArea) uploadArea.classList.add('hidden');
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
isRendering = true; isRendering = true;
console.log('PDF Multi Tool: Starting render, isRendering set to true');
renderCancelled = false; renderCancelled = false;
let totalPages = 0;
// Cleanup previous observers let currentPage = 0;
cleanupLazyRendering();
showLoading(0, 100);
try { try {
// First pass: count total pages
const pdfDocs: PDFLibDocument[] = [];
for (const file of files) { for (const file of files) {
if (renderCancelled) break;
try { try {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer); const pdfDoc = await PDFLibDocument.load(arrayBuffer);
currentPdfDocs.push(pdfDoc); pdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1; totalPages += pdfDoc.getPageCount();
const pdfBytes = await pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) }).promise;
const numPages = pdfjsDoc.numPages;
// Pre-fill allPages with placeholders to maintain order/state
const startIndex = allPages.length;
for (let i = 0; i < numPages; i++) {
allPages.push({
id: generateId(),
pdfIndex,
pageIndex: i,
rotation: 0,
visualRotation: 0,
canvas: null, // Will be filled when rendered
pdfDoc,
originalPageIndex: i,
fileName: file.name,
});
}
await renderPagesProgressively(
pdfjsDoc,
pagesContainer,
(canvas, pageNumber) => {
const globalIndex = startIndex + pageNumber - 1;
if (allPages[globalIndex]) {
allPages[globalIndex].canvas = canvas;
}
return createPageElement(canvas, globalIndex);
},
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '400px',
onProgress: (current, total) => {
showLoading(current, total);
},
onBatchComplete: () => {
createIcons({ icons });
},
shouldCancel: () => renderCancelled, // Pass cancellation check
}
);
} catch (e) { } catch (e) {
console.error(`Failed to load PDF ${file.name}:`, e); console.error(`Failed to load PDF ${file.name}:`, e);
showModal('Error', `Failed to load ${file.name}. The file may be corrupted.`, 'error'); showModal('Error', `Failed to load ${file.name}. The file may be corrupted.`, 'error');
} }
} }
// Second pass: render pages
for (const pdfDoc of pdfDocs) {
if (renderCancelled) break;
currentPdfDocs.push(pdfDoc);
const numPages = pdfDoc.getPageCount();
for (let i = 0; i < numPages; i++) {
if (renderCancelled) break;
currentPage++;
showLoading(currentPage, totalPages);
await renderPage(pdfDoc, i, currentPdfDocs.length - 1);
}
}
if (!renderCancelled) { if (!renderCancelled) {
setupSortable(); setupSortable();
createIcons({ icons }); createIcons({ icons });
@@ -415,13 +320,9 @@ async function loadPdfs(files: File[]) {
} finally { } finally {
hideLoading(); hideLoading();
isRendering = false; isRendering = false;
console.log('PDF Multi Tool: Render finished/cancelled, isRendering set to false');
if (renderCancelled) { if (renderCancelled) {
renderCancelled = false; renderCancelled = false;
} }
if (allPages.length === 0) {
resetAll();
}
} }
} }
@@ -429,63 +330,79 @@ function getCacheKey(pdfIndex: number, pageIndex: number): string {
return `${pdfIndex}-${pageIndex}`; return `${pdfIndex}-${pageIndex}`;
} }
// Wrapper for compatibility with updatePageDisplay async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: number) {
const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return;
// Check cache first
const cacheKey = getCacheKey(pdfIndex, pageIndex);
let canvas: HTMLCanvasElement;
if (pageCanvasCache.has(cacheKey)) {
canvas = pageCanvasCache.get(cacheKey)!;
} else {
const pdfBytes = await pdfDoc.save();
const pdf = await pdfjsLib.getDocument({ data: new Uint8Array(pdfBytes) }).promise;
const page = await pdf.getPage(pageIndex + 1);
const viewport = page.getViewport({ scale: 0.5, rotation: 0 });
canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
if (!context) return;
await page.render({
canvasContext: context,
viewport,
background: 'white',
canvas
}).promise;
// Cache the canvas
pageCanvasCache.set(cacheKey, canvas);
}
const pageData: PageData = {
pdfIndex,
pageIndex,
rotation: 0, // Actual rotation to apply when saving PDF
visualRotation: 0, // Visual rotation for display only
canvas,
pdfDoc,
originalPageIndex: pageIndex,
};
allPages.push(pageData);
createPageCard(pageData, allPages.length - 1);
}
function createPageCard(pageData: PageData, index: number) { function createPageCard(pageData: PageData, index: number) {
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; if (!pagesContainer) return;
const card = createPageElement(pageData.canvas, index);
pagesContainer.appendChild(card);
createIcons({ icons });
}
// Modified to return the element instead of appending it
function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTMLElement {
const pageData = allPages[index];
if (!pageData) {
console.error(`Page data not found for index ${index}`);
return document.createElement('div');
}
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'bg-gray-800 rounded-lg border-2 border-gray-700 p-2 relative group cursor-move'; card.className = 'bg-gray-800 rounded-lg border-2 border-gray-700 p-2 relative group cursor-move';
card.dataset.pageIndex = index.toString(); card.dataset.pageIndex = index.toString();
card.dataset.pageId = pageData.id; // Set ID for reconciliation
// Add attributes for lazy loading identification
card.dataset.pageNumber = (pageData.pageIndex + 1).toString();
if (pageData.fileName) {
card.dataset.fileName = pageData.fileName;
}
if (!pageData.canvas) {
card.dataset.lazyLoad = 'true';
}
if (selectedPages.has(index)) { if (selectedPages.has(index)) {
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500'); card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
} }
// Page preview
const preview = document.createElement('div'); const preview = document.createElement('div');
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative h-36 sm:h-64'; preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative';
preview.style.minHeight = '160px';
preview.style.height = '250px';
if (canvas) { const previewCanvas = pageData.canvas;
const previewCanvas = canvas;
previewCanvas.className = 'max-w-full max-h-full object-contain'; previewCanvas.className = 'max-w-full max-h-full object-contain';
// Apply visual rotation using CSS transform
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`; previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
previewCanvas.style.transition = 'transform 0.2s ease'; previewCanvas.style.transition = 'transform 0.2s ease';
preview.appendChild(previewCanvas); preview.appendChild(previewCanvas);
} else {
// Show loading placeholder if canvas is null
const loading = document.createElement('div');
loading.className = 'flex flex-col items-center justify-center text-gray-400';
loading.innerHTML = `
<i data-lucide="loader" class="w-8 h-8 animate-spin mb-2"></i>
<span class="text-xs">Loading...</span>
`;
preview.appendChild(loading);
preview.classList.add('bg-gray-700'); // Darker background for loading
}
// Page info // Page info
const info = document.createElement('div'); const info = document.createElement('div');
@@ -574,16 +491,9 @@ function createPageElement(canvas: HTMLCanvasElement | null, index: number): HTM
actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn); actionsInner.append(rotateLeftBtn, rotateBtn, duplicateBtn, insertBtn, splitBtn, deleteBtn);
card.append(preview, info, actions, selectBtn); card.append(preview, info, actions, selectBtn);
pagesContainer.appendChild(card);
// Check for split marker createIcons({ icons });
if (splitMarkers.has(index)) {
const marker = document.createElement('div');
marker.className = 'split-marker absolute -right-3 top-0 bottom-0 w-6 flex items-center justify-center z-20 pointer-events-none';
marker.innerHTML = '<div class="h-full w-0.5 border-l-2 border-dashed border-blue-400"></div>';
card.appendChild(marker);
}
return card;
} }
function setupSortable() { function setupSortable() {
@@ -602,9 +512,6 @@ function setupSortable() {
fallbackTolerance: 3, fallbackTolerance: 3,
delay: 200, delay: 200,
delayOnTouchOnly: true, delayOnTouchOnly: true,
scroll: document.getElementById('main-scroll-container'),
scrollSensitivity: 100, // Increase sensitivity for smoother scrolling
bubbleScroll: false, // Prevent bubbling scroll to parent
onEnd: (evt) => { onEnd: (evt) => {
const oldIndex = evt.oldIndex!; const oldIndex = evt.oldIndex!;
const newIndex = evt.newIndex!; const newIndex = evt.newIndex!;
@@ -693,7 +600,6 @@ function duplicatePage(index: number) {
const newPageData: PageData = { const newPageData: PageData = {
...originalPageData, ...originalPageData,
id: generateId(), // New ID for the duplicate
canvas: newCanvas, canvas: newCanvas,
}; };
@@ -737,65 +643,18 @@ async function handleInsertPdf(e: Event) {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer); const pdfDoc = await PDFLibDocument.load(arrayBuffer);
currentPdfDocs.push(pdfDoc); currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1;
// Load PDF.js document for rendering
const pdfBytes = await pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: new Uint8Array(pdfBytes) }).promise;
const numPages = pdfjsDoc.numPages;
const numPages = pdfDoc.getPageCount();
const newPages: PageData[] = []; const newPages: PageData[] = [];
for (let i = 0; i < numPages; i++) { for (let i = 0; i < numPages; i++) {
newPages.push({ // Use the existing renderPage function, which adds to allPages
id: generateId(), await renderPage(pdfDoc, i, currentPdfDocs.length - 1);
pdfIndex, // Move the newly added page data to the temporary array
pageIndex: i, newPages.push(allPages.pop()!);
rotation: 0,
visualRotation: 0,
canvas: null, // Placeholder
pdfDoc,
originalPageIndex: i,
fileName: file.name,
});
} }
// Insert new pages into allPages
allPages.splice(insertAfterIndex + 1, 0, ...newPages); allPages.splice(insertAfterIndex + 1, 0, ...newPages);
// Update display to show placeholders immediately
updatePageDisplay(); updatePageDisplay();
// Render pages progressively
for (let i = 0; i < numPages; i++) {
const globalIndex = insertAfterIndex + 1 + i;
// Render page
const canvas = await renderPageToCanvas(pdfjsDoc, i + 1);
// Update data
if (allPages[globalIndex]) {
allPages[globalIndex].canvas = canvas;
// Update UI if card exists
const pagesContainer = document.getElementById('pages-container');
const card = pagesContainer?.querySelector(`div[data-page-index="${globalIndex}"]`);
if (card) {
const preview = card.querySelector('.bg-gray-700') || card.querySelector('.bg-white');
if (preview) {
// Re-create the preview content
preview.innerHTML = '';
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative h-36 sm:h-64';
const previewCanvas = canvas;
previewCanvas.className = 'max-w-full max-h-full object-contain';
previewCanvas.style.transform = `rotate(${allPages[globalIndex].visualRotation}deg)`;
previewCanvas.style.transition = 'transform 0.2s ease';
preview.appendChild(previewCanvas);
}
}
}
}
} catch (e) { } catch (e) {
console.error('Failed to insert PDF:', e); console.error('Failed to insert PDF:', e);
showModal('Error', 'Failed to insert PDF. The file may be corrupted.', 'error'); showModal('Error', 'Failed to insert PDF. The file may be corrupted.', 'error');
@@ -836,7 +695,6 @@ function addBlankPage() {
} }
const blankPageData: PageData = { const blankPageData: PageData = {
id: generateId(),
pdfIndex: -1, pdfIndex: -1,
pageIndex: -1, pageIndex: -1,
rotation: 0, rotation: 0,
@@ -844,7 +702,6 @@ function addBlankPage() {
canvas, canvas,
pdfDoc: null as any, pdfDoc: null as any,
originalPageIndex: -1, originalPageIndex: -1,
fileName: '', // Blank page has no file
}; };
allPages.push(blankPageData); allPages.push(blankPageData);
@@ -859,26 +716,11 @@ function bulkRotate(delta: number) {
selectedPages.forEach(index => { selectedPages.forEach(index => {
const pageData = allPages[index]; const pageData = allPages[index];
if (pageData) {
// Update state
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360; pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
pageData.rotation = (pageData.rotation + delta + 360) % 360; pageData.rotation = (pageData.rotation + delta + 360) % 360;
// Update DOM immediately if it exists
const pagesContainer = document.getElementById('pages-container');
const card = pagesContainer?.querySelector(`div[data-page-index="${index}"]`);
if (card) {
const canvas = card.querySelector('canvas');
if (canvas) {
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
}
// If no canvas (placeholder), the state update is enough.
// When it eventually renders, createPageElement will use the new rotation.
}
}
}); });
// TODO@ALAM - Do NOT call updatePageDisplay() as it destroys lazy loading observers updatePageDisplay();
} }
function bulkDelete() { function bulkDelete() {
@@ -927,6 +769,14 @@ function bulkSplit() {
updatePageDisplay(); updatePageDisplay();
} }
async function bulkDownload() {
if (selectedPages.size === 0) {
showModal('No Selection', 'Please select pages to download.', 'info');
return;
}
const indices = Array.from(selectedPages);
await downloadPagesAsPdf(indices, 'selected-pages.pdf');
}
async function downloadAll() { async function downloadAll() {
if (allPages.length === 0) { if (allPages.length === 0) {
@@ -976,10 +826,6 @@ async function downloadSplitPdfs() {
for (const index of segment) { for (const index of segment) {
const pageData = allPages[index]; const pageData = allPages[index];
if (!pageData) {
console.warn(`Page data missing for index ${index}`);
continue;
}
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
const page = newPdf.addPage(copiedPage); const page = newPdf.addPage(copiedPage);
@@ -1005,8 +851,6 @@ async function downloadSplitPdfs() {
} catch (e) { } catch (e) {
console.error('Failed to create split PDFs:', e); console.error('Failed to create split PDFs:', e);
showModal('Error', 'Failed to create split PDFs.', 'error'); showModal('Error', 'Failed to create split PDFs.', 'error');
} finally {
hideLoading(); // Ensure loader is hidden if we used it (though showModal replaces it)
} }
} }
@@ -1016,10 +860,6 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
for (const index of indices) { for (const index of indices) {
const pageData = allPages[index]; const pageData = allPages[index];
if (!pageData) {
console.warn(`Page data missing for index ${index}`);
continue;
}
if (pageData.pdfDoc && pageData.originalPageIndex >= 0) { if (pageData.pdfDoc && pageData.originalPageIndex >= 0) {
// Copy page from original PDF // Copy page from original PDF
const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]); const [copiedPage] = await newPdf.copyPages(pageData.pdfDoc, [pageData.originalPageIndex]);
@@ -1038,7 +878,6 @@ async function downloadPagesAsPdf(indices: number[], filename: string) {
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }); const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
downloadFile(blob, filename); downloadFile(blob, filename);
showModal('Success', 'PDF downloaded successfully.', 'success');
} catch (e) { } catch (e) {
console.error('Failed to create PDF:', e); console.error('Failed to create PDF:', e);
showModal('Error', 'Failed to create PDF.', 'error'); showModal('Error', 'Failed to create PDF.', 'error');
@@ -1049,130 +888,15 @@ function updatePageDisplay() {
const pagesContainer = document.getElementById('pages-container'); const pagesContainer = document.getElementById('pages-container');
if (!pagesContainer) return; if (!pagesContainer) return;
// 1. Create a map of existing elements by ID pagesContainer.innerHTML = '';
const existingElements = new Map<string, HTMLElement>();
Array.from(pagesContainer.children).forEach((child) => {
const el = child as HTMLElement;
if (el.dataset.pageId) {
existingElements.set(el.dataset.pageId, el);
}
});
// 2. Iterate through allPages and reconcile DOM
allPages.forEach((pageData, index) => { allPages.forEach((pageData, index) => {
let card = existingElements.get(pageData.id); createPageCard(pageData, index);
if (card) {
// Element exists, update it
existingElements.delete(pageData.id); // Remove from map so we know it's still in use
// Check if position changed
if (pagesContainer.children[index] !== card) {
if (index < pagesContainer.children.length) {
pagesContainer.insertBefore(card, pagesContainer.children[index]);
} else {
pagesContainer.appendChild(card);
}
}
// Update index-dependent attributes
card.dataset.pageIndex = index.toString();
const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2');
if (info) info.textContent = `Page ${index + 1} `;
// Update selection state
const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]');
if (selectBtn) {
if (selectedPages.has(index)) {
card.classList.add('border-indigo-500', 'ring-2', 'ring-indigo-500');
selectBtn.innerHTML = '<i data-lucide="check-square" class="w-4 h-4 text-indigo-400"></i>';
} else {
card.classList.remove('border-indigo-500', 'ring-2', 'ring-indigo-500');
selectBtn.innerHTML = '<i data-lucide="square" class="w-4 h-4 text-gray-200"></i>';
}
// Update click handler to use new index
(selectBtn as HTMLElement).onclick = (e) => {
e.stopPropagation();
toggleSelectOptimized(index);
};
}
// Update visual rotation
const canvas = card.querySelector('canvas');
if (canvas) {
canvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
}
// Update action buttons
const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90');
if (actionsInner) {
const buttons = actionsInner.querySelectorAll('button');
if (buttons[0]) (buttons[0] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); };
if (buttons[1]) (buttons[1] as HTMLElement).onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); };
if (buttons[2]) (buttons[2] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); };
if (buttons[3]) (buttons[3] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); };
if (buttons[4]) (buttons[4] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); };
if (buttons[5]) (buttons[5] as HTMLElement).onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); };
}
} else {
// Element doesn't exist, create it
card = createPageElement(pageData.canvas, index);
card.dataset.pageId = pageData.id; // IMPORTANT: Set the ID
if (index < pagesContainer.children.length) {
pagesContainer.insertBefore(card, pagesContainer.children[index]);
} else {
pagesContainer.appendChild(card);
}
}
}); });
// 3. Remove remaining elements (deleted pages)
existingElements.forEach((el) => el.remove());
setupSortable(); setupSortable();
renderSplitMarkers(); renderSplitMarkers();
createIcons({ icons }); createIcons({ icons });
} }
function updatePageNumbers() { function updatePageNumbers() {
const pagesContainer = document.getElementById('pages-container'); updatePageDisplay();
if (!pagesContainer) return;
const cards = Array.from(pagesContainer.children) as HTMLElement[];
cards.forEach((card, index) => {
// Update data attribute
card.dataset.pageIndex = index.toString();
// Update visible page number text
const info = card.querySelector('.text-xs.text-gray-400.text-center.mb-2');
if (info) {
info.textContent = `Page ${index + 1} `;
}
// Re-attach event listeners for buttons
// We need to find the buttons and update their onclick handlers
// This is necessary because the original handlers captured the old index
const selectBtn = card.querySelector('button[class*="absolute top-2 right-2"]') as HTMLButtonElement;
if (selectBtn) {
selectBtn.onclick = (e) => {
e.stopPropagation();
toggleSelectOptimized(index);
};
}
const actionsInner = card.querySelector('.flex.items-center.gap-1.bg-gray-900\\/90');
if (actionsInner) {
const buttons = actionsInner.querySelectorAll('button');
// Order: Rotate Left, Rotate Right, Duplicate, Insert, Split, Delete
if (buttons[0]) buttons[0].onclick = (e) => { e.stopPropagation(); rotatePage(index, -90); };
if (buttons[1]) buttons[1].onclick = (e) => { e.stopPropagation(); rotatePage(index, 90); };
if (buttons[2]) buttons[2].onclick = (e) => { e.stopPropagation(); snapshot(); duplicatePage(index); };
if (buttons[3]) buttons[3].onclick = (e) => { e.stopPropagation(); snapshot(); insertPdfAfter(index); };
if (buttons[4]) buttons[4].onclick = (e) => { e.stopPropagation(); snapshot(); toggleSplitMarker(index); renderSplitMarkers(); };
if (buttons[5]) buttons[5].onclick = (e) => { e.stopPropagation(); snapshot(); deletePage(index); };
}
});
} }
+4 -6
View File
@@ -1,10 +1,7 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
/** /**
* Creates a BMP file buffer from raw pixel data (ImageData). * Creates a BMP file buffer from raw pixel data (ImageData).
@@ -56,7 +53,8 @@ function encodeBMP(imageData: any) {
export async function pdfToBmp() { export async function pdfToBmp() {
showLoader('Converting PDF to BMP images...'); showLoader('Converting PDF to BMP images...');
try { try {
const pdf = await getPDFDocument( // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
const zip = new JSZip(); const zip = new JSZip();
@@ -71,7 +69,7 @@ export async function pdfToBmp() {
canvas.width = viewport.width; canvas.width = viewport.width;
// Render the PDF page directly to the canvas // Render the PDF page directly to the canvas
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise; await page.render({ canvasContext: context, viewport: viewport }).promise;
// Get the raw pixel data from this canvas // Get the raw pixel data from this canvas
const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
+5 -6
View File
@@ -1,10 +1,8 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function pdfToGreyscale() { export async function pdfToGreyscale() {
if (!state.pdfDoc) { if (!state.pdfDoc) {
@@ -15,7 +13,8 @@ export async function pdfToGreyscale() {
try { try {
const newPdfDoc = await PDFLibDocument.create(); const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save(); const pdfBytes = await state.pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise; // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) { for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i); const page = await pdfjsDoc.getPage(i);
@@ -25,7 +24,7 @@ export async function pdfToGreyscale() {
canvas.height = viewport.height; canvas.height = viewport.height;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport, canvas }).promise; await page.render({ canvasContext: ctx, viewport }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; const data = imageData.data;
+4 -7
View File
@@ -1,16 +1,13 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToJpg() { export async function pdfToJpg() {
showLoader('Converting to JPG...'); showLoader('Converting to JPG...');
try { try {
const pdf = await getPDFDocument( // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
const zip = new JSZip(); const zip = new JSZip();
@@ -26,7 +23,7 @@ export async function pdfToJpg() {
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise; await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise((resolve) => const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', quality) canvas.toBlob(resolve, 'image/jpeg', quality)
+2 -2
View File
@@ -1,6 +1,5 @@
import JSZip from 'jszip' import JSZip from 'jszip'
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers'; import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
const worker = new Worker(new URL('/workers/pdf-to-json.worker.js', import.meta.url)); const worker = new Worker(new URL('/workers/pdf-to-json.worker.js', import.meta.url));
@@ -150,5 +149,6 @@ if (backToToolsBtn) {
convertBtn.addEventListener('click', convertPDFsToJSON) convertBtn.addEventListener('click', convertPDFsToJSON)
// Initialize
showStatus('Select PDF files to get started', 'info') showStatus('Select PDF files to get started', 'info')
initializeGlobalShortcuts()
+3 -2
View File
@@ -1,5 +1,5 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
export async function pdfToMarkdown() { export async function pdfToMarkdown() {
@@ -7,7 +7,8 @@ export async function pdfToMarkdown() {
try { try {
const file = state.files[0]; const file = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file); const arrayBuffer = await readFileAsArrayBuffer(file);
const pdf = await getPDFDocument({ data: arrayBuffer }).promise; // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let markdown = ''; let markdown = '';
for (let i = 1; i <= pdf.numPages; i++) { for (let i = 1; i <= pdf.numPages; i++) {
+4 -7
View File
@@ -1,16 +1,13 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToPng() { export async function pdfToPng() {
showLoader('Converting to PNG...'); showLoader('Converting to PNG...');
try { try {
const pdf = await getPDFDocument( // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
const zip = new JSZip(); const zip = new JSZip();
@@ -25,7 +22,7 @@ export async function pdfToPng() {
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; canvas.width = viewport.width;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise; await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise((resolve) => const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/png') canvas.toBlob(resolve, 'image/png')
); );
+2 -4
View File
@@ -1,16 +1,14 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import UTIF from 'utif'; import UTIF from 'utif';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToTiff() { export async function pdfToTiff() {
showLoader('Converting PDF to TIFF...'); showLoader('Converting PDF to TIFF...');
try { try {
const pdf = await getPDFDocument( const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
const zip = new JSZip(); const zip = new JSZip();
+4 -7
View File
@@ -1,16 +1,13 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToWebp() { export async function pdfToWebp() {
showLoader('Converting to WebP...'); showLoader('Converting to WebP...');
try { try {
const pdf = await getPDFDocument( // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(
await readFileAsArrayBuffer(state.files[0]) await readFileAsArrayBuffer(state.files[0])
).promise; ).promise;
const zip = new JSZip(); const zip = new JSZip();
@@ -21,7 +18,7 @@ export async function pdfToWebp() {
canvas.height = viewport.height; canvas.height = viewport.height;
canvas.width = viewport.width; canvas.width = viewport.width;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise; await page.render({ canvasContext: context, viewport: viewport }).promise;
const qualityInput = document.getElementById('webp-quality') as HTMLInputElement; const qualityInput = document.getElementById('webp-quality') as HTMLInputElement;
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9; const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9;
+2 -4
View File
@@ -1,12 +1,10 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, parsePageRanges, getPDFDocument } from '../utils/helpers.js'; import { downloadFile, parsePageRanges } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument, PageSizes } from 'pdf-lib'; import { PDFDocument, PageSizes } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const posterizeState = { const posterizeState = {
pdfJsDoc: null, pdfJsDoc: null,
pageSnapshots: {}, pageSnapshots: {},
@@ -123,7 +121,7 @@ export async function setupPosterizeTool() {
.getPageCount() .getPageCount()
.toString(); .toString();
const pdfBytes = await state.pdfDoc.save(); const pdfBytes = await state.pdfDoc.save();
posterizeState.pdfJsDoc = await getPDFDocument({ data: pdfBytes }) posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes })
.promise; .promise;
posterizeState.pageSnapshots = {}; posterizeState.pageSnapshots = {};
posterizeState.currentPage = 1; posterizeState.currentPage = 1;
+2 -4
View File
@@ -1,12 +1,10 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument } from 'pdf-lib'; import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { PDFPageProxy } from 'pdfjs-dist/types/src/display/api.js'; import { PDFPageProxy } from 'pdfjs-dist/types/src/display/api.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let analysisCache = []; let analysisCache = [];
async function isPageBlank(page: PDFPageProxy, threshold: number) { async function isPageBlank(page: PDFPageProxy, threshold: number) {
@@ -40,7 +38,7 @@ async function analyzePages() {
showLoader('Analyzing for blank pages...'); showLoader('Analyzing for blank pages...');
const pdfBytes = await state.pdfDoc.save(); const pdfBytes = await state.pdfDoc.save();
const pdf = await getPDFDocument({ data: pdfBytes }).promise; const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
analysisCache = []; analysisCache = [];
const promises = []; const promises = [];
+7 -11
View File
@@ -1,7 +1,6 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, resetAndReloadTool } from '../utils/helpers.js'; import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { getRotationState, resetRotationState } from '../handlers/fileHandler.js';
import { degrees } from 'pdf-lib'; import { degrees } from 'pdf-lib';
@@ -9,11 +8,12 @@ export async function rotate() {
showLoader('Applying rotations...'); showLoader('Applying rotations...');
try { try {
const pages = state.pdfDoc.getPages(); const pages = state.pdfDoc.getPages();
const rotationStateArray = getRotationState(); document.querySelectorAll('.page-rotator-item').forEach((item) => {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
// Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated const pageIndex = parseInt(item.dataset.pageIndex);
rotationStateArray.forEach((rotation, pageIndex) => { // @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
if (rotation !== 0 && pages[pageIndex]) { const rotation = parseInt(item.dataset.rotation || '0');
if (rotation !== 0) {
const currentRotation = pages[pageIndex].getRotation().angle; const currentRotation = pages[pageIndex].getRotation().angle;
pages[pageIndex].setRotation(degrees(currentRotation + rotation)); pages[pageIndex].setRotation(degrees(currentRotation + rotation));
} }
@@ -24,10 +24,6 @@ export async function rotate() {
new Blob([rotatedPdfBytes], { type: 'application/pdf' }), new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
'rotated.pdf' 'rotated.pdf'
); );
resetAndReloadTool(() => {
resetRotationState();
});
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'Could not apply rotations.'); showAlert('Error', 'Could not apply rotations.');
-198
View File
@@ -1,198 +0,0 @@
import { setupToolInterface } from '../handlers/toolSelectionHandler.js';
import { categories } from '../config/tools.js';
export class ShortcutsManager {
private static STORAGE_KEY = 'bentopdf_shortcuts';
private static shortcuts: Map<string, string> = new Map();
private static defaultShortcuts: Map<string, string> = new Map();
private static getToolId(tool: any): string {
if (tool.id) return tool.id;
if (tool.href) {
// Extract filename without extension from href
const match = tool.href.match(/\/([^/]+)\.html$/);
return match ? match[1] : tool.href;
}
return 'unknown';
}
static init() {
this.loadDefaults();
this.loadFromStorage();
this.setupGlobalListener();
}
private static loadDefaults() {
this.defaultShortcuts.set('merge', 'mod+shift+m');
this.defaultShortcuts.set('split', 'mod+shift+s');
this.defaultShortcuts.set('compress', 'mod+shift+c');
}
private static loadFromStorage() {
const stored = localStorage.getItem(this.STORAGE_KEY);
if (stored) {
try {
const parsed = JSON.parse(stored);
this.shortcuts = new Map(Object.entries(parsed));
const allTools = categories.flatMap(c => c.tools);
const validToolIds = allTools.map(t => this.getToolId(t));
for (const [toolId, _] of this.shortcuts.entries()) {
if (!validToolIds.includes(toolId)) {
this.shortcuts.delete(toolId);
}
}
if (this.shortcuts.size !== Object.keys(parsed).length) {
this.save();
}
} catch (e) {
console.error('Failed to parse shortcuts from local storage', e);
this.shortcuts = new Map(this.defaultShortcuts);
}
} else {
this.shortcuts = new Map(this.defaultShortcuts);
}
}
static save() {
const obj = Object.fromEntries(this.shortcuts);
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(obj));
}
static reset() {
this.shortcuts = new Map(this.defaultShortcuts);
this.save();
// Dispatch event to update UI if needed
window.dispatchEvent(new CustomEvent('shortcuts-updated'));
}
static getShortcut(toolId: string): string | undefined {
return this.shortcuts.get(toolId);
}
static findToolByShortcut(key: string): string | undefined {
for (const [id, k] of this.shortcuts.entries()) {
if (k === key) {
return id;
}
}
return undefined;
}
static setShortcut(toolId: string, key: string) {
if (key) {
this.shortcuts.set(toolId, key);
} else {
this.shortcuts.delete(toolId);
}
this.save();
window.dispatchEvent(new CustomEvent('shortcuts-updated'));
}
static getAllShortcuts(): Map<string, string> {
return this.shortcuts;
}
static exportSettings() {
// Create a map with all tools, defaulting to empty string if not set
const exportObj: Record<string, string> = {};
const allTools = categories.flatMap(c => c.tools);
allTools.forEach(tool => {
const toolId = this.getToolId(tool);
exportObj[toolId] = this.shortcuts.get(toolId) || '';
});
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportObj, null, 2));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "bentopdf_shortcuts.json");
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
static importSettings(jsonString: string): boolean {
try {
const parsed = JSON.parse(jsonString);
for (const key in parsed) {
if (typeof parsed[key] !== 'string') {
throw new Error('Invalid shortcut format');
}
}
this.shortcuts = new Map(Object.entries(parsed));
this.save();
window.dispatchEvent(new CustomEvent('shortcuts-updated'));
return true;
} catch (e) {
console.error('Import failed', e);
return false;
}
}
private static setupGlobalListener() {
window.addEventListener('keydown', (e) => {
// Ignore if typing in an input or textarea
const target = e.target as HTMLElement;
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
return;
}
// Build key string
const keys: string[] = [];
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
// On Mac: metaKey = Command, ctrlKey = Control
// On Windows/Linux: metaKey is rare, ctrlKey = Ctrl
if (isMac) {
if (e.metaKey) keys.push('mod'); // Command on Mac
if (e.ctrlKey) keys.push('ctrl'); // Control on Mac (separate from Command)
} else {
if (e.ctrlKey || e.metaKey) keys.push('mod'); // Ctrl on Windows/Linux
}
if (e.altKey) keys.push('alt');
if (e.shiftKey) keys.push('shift');
let key = e.key.toLowerCase();
if (e.altKey && e.code) {
if (e.code.startsWith('Key')) {
key = e.code.slice(3).toLowerCase();
} else if (e.code.startsWith('Digit')) {
key = e.code.slice(5);
}
}
if (!['control', 'shift', 'alt', 'meta'].includes(key)) {
keys.push(key);
}
const combo = keys.join('+');
for (const [toolId, shortcut] of this.shortcuts.entries()) {
if (shortcut === combo) {
e.preventDefault();
e.stopPropagation();
// Find the tool in categories
const allTools = categories.flatMap(c => c.tools);
const tool = allTools.find(t => this.getToolId(t) === toolId);
if (tool && (tool as any).href) {
const href = (tool as any).href;
window.location.href = href.startsWith('/') ? href : `/${href}`;
} else if (tool && (tool as any).id) {
if (window.location.pathname === '/' || window.location.pathname === '/index.html') {
setupToolInterface(toolId);
} else {
window.location.href = `/#tool-${toolId}`;
}
}
return;
}
}
}, { capture: true });
}
}
+7 -54
View File
@@ -1,4 +1,3 @@
import { PDFDocument } from 'pdf-lib';
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js'; import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
@@ -41,21 +40,9 @@ export async function setupSignTool() {
const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }); const blob = new Blob([pdfBytes as BlobPart], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob); const blobUrl = URL.createObjectURL(blob);
try { const viewerBase = '/pdfjs-viewer/viewer.html';
const existingPrefsRaw = localStorage.getItem('pdfjs.preferences');
const existingPrefs = existingPrefsRaw ? JSON.parse(existingPrefsRaw) : {};
delete (existingPrefs as any).annotationEditorMode;
const newPrefs = {
...existingPrefs,
enableSignatureEditor: true,
enablePermissions: false,
};
localStorage.setItem('pdfjs.preferences', JSON.stringify(newPrefs));
} catch { }
const viewerUrl = new URL('/pdfjs-viewer/viewer.html', window.location.origin);
const query = new URLSearchParams({ file: blobUrl }); const query = new URLSearchParams({ file: blobUrl });
iframe.src = `${viewerUrl.toString()}?${query.toString()}`; iframe.src = `${viewerBase}?${query.toString()}`;
iframe.onload = () => { iframe.onload = () => {
hideLoader(); hideLoader();
@@ -65,27 +52,23 @@ export async function setupSignTool() {
if (viewerWindow && viewerWindow.PDFViewerApplication) { if (viewerWindow && viewerWindow.PDFViewerApplication) {
const app = viewerWindow.PDFViewerApplication; const app = viewerWindow.PDFViewerApplication;
const doc = viewerWindow.document; const doc = viewerWindow.document;
const eventBus = app.eventBus;
eventBus?._on('annotationeditoruimanager', () => {
const editorModeButtons = doc.getElementById('editorModeButtons'); const editorModeButtons = doc.getElementById('editorModeButtons');
editorModeButtons?.classList.remove('hidden'); editorModeButtons?.classList.remove('hidden');
const editorSignature = doc.getElementById('editorSignature'); const editorSignature = doc.getElementById('editorSignature');
editorSignature?.removeAttribute('hidden'); editorSignature?.removeAttribute('hidden');
const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null; const editorSignatureButton = doc.getElementById('editorSignatureButton') as HTMLButtonElement | null;
if (editorSignatureButton) { if (editorSignatureButton) {
editorSignatureButton.disabled = false; editorSignatureButton.disabled = false;
} }
const editorStamp = doc.getElementById('editorStamp'); const editorStamp = doc.getElementById('editorStamp');
editorStamp?.removeAttribute('hidden'); editorStamp?.removeAttribute('hidden');
const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null; const editorStampButton = doc.getElementById('editorStampButton') as HTMLButtonElement | null;
if (editorStampButton) { if (editorStampButton) {
editorStampButton.disabled = false; editorStampButton.disabled = false;
} }
try {
const highlightBtn = doc.getElementById('editorHighlightButton') as HTMLButtonElement | null;
highlightBtn?.click();
} catch { }
});
} }
} catch (e) { } catch (e) {
console.error('Could not initialize base PDF.js viewer for signing:', e); console.error('Could not initialize base PDF.js viewer for signing:', e);
@@ -116,41 +99,11 @@ export async function applyAndSaveSignatures() {
return; return;
} }
const app = viewerWindow.PDFViewerApplication;
const flattenCheckbox = document.getElementById('flatten-signature-toggle') as HTMLInputElement | null;
const shouldFlatten = flattenCheckbox?.checked;
if (shouldFlatten) {
showLoader('Flattening and saving PDF...');
const rawPdfBytes = await app.pdfDocument.saveDocument(app.pdfDocument.annotationStorage);
const pdfBytes = new Uint8Array(rawPdfBytes);
const pdfDoc = await PDFDocument.load(pdfBytes);
pdfDoc.getForm().flatten();
const flattenedPdfBytes = await pdfDoc.save();
const blob = new Blob([flattenedPdfBytes as BlobPart], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `signed_flattened_${state.files[0].name}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
hideLoader();
} else {
// Delegate to the built-in download behavior of the base viewer. // Delegate to the built-in download behavior of the base viewer.
const app = viewerWindow.PDFViewerApplication;
app.eventBus?.dispatch('download', { source: app }); app.eventBus?.dispatch('download', { source: app });
}
} catch (error) { } catch (error) {
console.error('Failed to export the signed PDF:', error); console.error('Failed to trigger download in base PDF.js viewer:', error);
hideLoader();
showAlert('Export failed', 'Could not export the signed PDF. Please try again.'); showAlert('Export failed', 'Could not export the signed PDF. Please try again.');
} }
} }
+18 -36
View File
@@ -1,11 +1,6 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { createIcons, icons } from 'lucide'; import { downloadFile } from '../utils/helpers.js';
import * as pdfjsLib from 'pdfjs-dist';
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
import { state } from '../state.js'; import { state } from '../state.js';
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.js';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
@@ -22,29 +17,35 @@ async function renderVisualSelector() {
container.textContent = ''; container.textContent = '';
// Cleanup any previous lazy loading observers
cleanupLazyRendering();
showLoader('Rendering page previews...'); showLoader('Rendering page previews...');
try { try {
const pdfData = await state.pdfDoc.save(); const pdfData = await state.pdfDoc.save();
const pdf = await getPDFDocument({ data: pdfData }).promise; // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.4 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({
canvasContext: canvas.getContext('2d'),
viewport: viewport,
}).promise;
// Function to create wrapper element for each page
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = wrapper.className =
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500'; 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = pageNumber - 1; wrapper.dataset.pageIndex = i - 1;
const img = document.createElement('img'); const img = document.createElement('img');
img.src = canvas.toDataURL(); img.src = canvas.toDataURL();
img.className = 'rounded-md w-full h-auto'; img.className = 'rounded-md w-full h-auto';
const p = document.createElement('p'); const p = document.createElement('p');
p.className = 'text-center text-xs mt-1 text-gray-300'; p.className = 'text-center text-xs mt-1 text-gray-300';
p.textContent = `Page ${pageNumber}`; p.textContent = `Page ${i}`;
wrapper.append(img, p); wrapper.append(img, p);
const handleSelection = (e: any) => { const handleSelection = (e: any) => {
@@ -68,31 +69,12 @@ async function renderVisualSelector() {
wrapper.addEventListener('touchstart', (e) => { wrapper.addEventListener('touchstart', (e) => {
e.preventDefault(); e.preventDefault();
}); });
container.appendChild(wrapper);
return wrapper;
};
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdf,
container,
createWrapper,
{
batchSize: 8,
useLazyLoading: true,
lazyLoadMargin: '400px',
onProgress: (current, total) => {
showLoader(`Rendering page previews: ${current}/${total}`);
},
onBatchComplete: () => {
createIcons({ icons });
} }
}
);
} catch (error) { } catch (error) {
console.error('Error rendering visual selector:', error); console.error('Error rendering visual selector:', error);
showAlert('Error', 'Failed to render page previews.'); showAlert('Error', 'Failed to render page previews.');
// Reset the flag on error so the user can try again. // 4. ADDED: Reset the flag on error so the user can try again.
visualSelectorRendered = false; visualSelectorRendered = false;
} finally { } finally {
hideLoader(); hideLoader();
-4
View File
@@ -1,6 +1,4 @@
import { downloadFile, formatBytes } from "../utils/helpers"; import { downloadFile, formatBytes } from "../utils/helpers";
import { initializeGlobalShortcuts } from "../utils/shortcuts-init.js";
const worker = new Worker('/workers/table-of-contents.worker.js'); const worker = new Worker('/workers/table-of-contents.worker.js');
@@ -204,5 +202,3 @@ if (backToToolsBtn) {
} }
generateBtn.addEventListener('click', generateTableOfContents); generateBtn.addEventListener('click', generateTableOfContents);
initializeGlobalShortcuts();
-5
View File
@@ -154,11 +154,6 @@ export async function setupTxtToPdfTool() {
uploadBtn.addEventListener('click', switchToUpload); uploadBtn.addEventListener('click', switchToUpload);
textBtn.addEventListener('click', switchToText); textBtn.addEventListener('click', switchToText);
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.onclick = txtToPdf;
}
} }
export async function txtToPdf() { export async function txtToPdf() {
+13 -551
View File
@@ -1,18 +1,17 @@
import { categories } from './config/tools.js'; import { categories } from './config/tools.js';
import { dom, switchView, hideAlert, showLoader, hideLoader, showAlert } from './ui.js'; import { dom, switchView, hideAlert } from './ui.js';
import { setupToolInterface } from './handlers/toolSelectionHandler.js'; import { setupToolInterface } from './handlers/toolSelectionHandler.js';
import { state, resetState } from './state.js';
import { ShortcutsManager } from './logic/shortcuts.js';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import '../css/styles.css'; import '../css/styles.css';
import { formatShortcutDisplay, formatStars } from './utils/helpers.js'; import { formatStars } from './utils/helpers.js';
import { APP_VERSION, injectVersion } from '../version.js';
const init = () => { const init = () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
// Handle simple mode - hide branding sections but keep logo and copyright
// Handle simple mode - hide branding sections but keep logo and copyright // Handle simple mode - hide branding sections but keep logo and copyright
if (__SIMPLE_MODE__) { if (__SIMPLE_MODE__) {
const hideBrandingSections = () => { const hideBrandingSections = () => {
@@ -29,11 +28,9 @@ const init = () => {
simpleNav.innerHTML = ` simpleNav.innerHTML = `
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-start items-center h-16"> <div class="flex justify-start items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8"> <img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">BentoPDF</span>
<a href="index.html">BentoPDF</a>
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -80,12 +77,6 @@ const init = () => {
supportSection.style.display = 'none'; supportSection.style.display = 'none';
} }
// Hide "Used by companies" section
const usedBySection = document.querySelector('.hide-section') as HTMLElement;
if (usedBySection) {
usedBySection.style.display = 'none';
}
// Hide footer but keep copyright // Hide footer but keep copyright
const footer = document.querySelector('footer'); const footer = document.querySelector('footer');
if (footer) { if (footer) {
@@ -103,7 +94,7 @@ const init = () => {
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version-simple">${APP_VERSION}</span> Version <span id="app-version-simple">1.6.2</span>
</p> </p>
</div> </div>
`; `;
@@ -140,16 +131,6 @@ const init = () => {
hideBrandingSections(); hideBrandingSections();
} }
// Hide shortcuts button on touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
const shortcutsBtn = document.getElementById('open-shortcuts-btn');
if (shortcutsBtn) {
shortcutsBtn.style.display = 'none';
}
}
dom.toolGrid.textContent = ''; dom.toolGrid.textContent = '';
categories.forEach((category) => { categories.forEach((category) => {
@@ -285,539 +266,20 @@ const init = () => {
}); });
} }
if (window.location.hash.startsWith('#tool-')) {
const toolId = window.location.hash.substring(6);
setTimeout(() => {
setupToolInterface(toolId);
history.replaceState(null, '', window.location.pathname);
}, 100);
}
createIcons({ icons }); createIcons({ icons });
console.log('Please share our tool and share the love!'); console.log('Please share our tool and share the love!');
const githubStarsElement = document.getElementById('github-stars');
const githubStarsElements = [ if (githubStarsElement && !__SIMPLE_MODE__) {
document.getElementById('github-stars-desktop'),
document.getElementById('github-stars-mobile')
];
if (githubStarsElements.some(el => el) && !__SIMPLE_MODE__) {
fetch('https://api.github.com/repos/alam00000/bentopdf') fetch('https://api.github.com/repos/alam00000/bentopdf')
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (data.stargazers_count !== undefined) { if (data.stargazers_count !== undefined) {
const formattedStars = formatStars(data.stargazers_count); githubStarsElement.textContent = formatStars(data.stargazers_count);
githubStarsElements.forEach(el => {
if (el) el.textContent = formattedStars;
});
} }
}) })
.catch(() => { .catch(() => {
githubStarsElements.forEach(el => { githubStarsElement.textContent = '-';
if (el) el.textContent = '-';
});
});
}
// Initialize Shortcuts System
ShortcutsManager.init();
// Tab switching for settings modal
const shortcutsTabBtn = document.getElementById('shortcuts-tab-btn');
const preferencesTabBtn = document.getElementById('preferences-tab-btn');
const shortcutsTabContent = document.getElementById('shortcuts-tab-content');
const preferencesTabContent = document.getElementById('preferences-tab-content');
const shortcutsTabFooter = document.getElementById('shortcuts-tab-footer');
const preferencesTabFooter = document.getElementById('preferences-tab-footer');
const resetShortcutsBtn = document.getElementById('reset-shortcuts-btn');
if (shortcutsTabBtn && preferencesTabBtn) {
shortcutsTabBtn.addEventListener('click', () => {
shortcutsTabBtn.classList.add('bg-indigo-600', 'text-white');
shortcutsTabBtn.classList.remove('text-gray-300');
preferencesTabBtn.classList.remove('bg-indigo-600', 'text-white');
preferencesTabBtn.classList.add('text-gray-300');
shortcutsTabContent?.classList.remove('hidden');
preferencesTabContent?.classList.add('hidden');
shortcutsTabFooter?.classList.remove('hidden');
preferencesTabFooter?.classList.add('hidden');
resetShortcutsBtn?.classList.remove('hidden');
});
preferencesTabBtn.addEventListener('click', () => {
preferencesTabBtn.classList.add('bg-indigo-600', 'text-white');
preferencesTabBtn.classList.remove('text-gray-300');
shortcutsTabBtn.classList.remove('bg-indigo-600', 'text-white');
shortcutsTabBtn.classList.add('text-gray-300');
preferencesTabContent?.classList.remove('hidden');
shortcutsTabContent?.classList.add('hidden');
preferencesTabFooter?.classList.remove('hidden');
shortcutsTabFooter?.classList.add('hidden');
resetShortcutsBtn?.classList.add('hidden');
});
}
// Full-width toggle functionality
const fullWidthToggle = document.getElementById('full-width-toggle') as HTMLInputElement;
const toolInterface = document.getElementById('tool-interface');
// Load saved preference
const savedFullWidth = localStorage.getItem('fullWidthMode') === 'true';
if (fullWidthToggle) {
fullWidthToggle.checked = savedFullWidth;
applyFullWidthMode(savedFullWidth);
}
function applyFullWidthMode(enabled: boolean) {
if (toolInterface) {
if (enabled) {
toolInterface.classList.remove('max-w-4xl');
} else {
toolInterface.classList.add('max-w-4xl');
}
}
// Apply to all page uploaders
const pageUploaders = document.querySelectorAll('#tool-uploader');
pageUploaders.forEach((uploader) => {
if (enabled) {
uploader.classList.remove('max-w-2xl', 'max-w-5xl');
} else {
// Restore original max-width (most are max-w-2xl, add-stamps is max-w-5xl)
if (!uploader.classList.contains('max-w-2xl') && !uploader.classList.contains('max-w-5xl')) {
uploader.classList.add('max-w-2xl');
}
}
});
}
if (fullWidthToggle) {
fullWidthToggle.addEventListener('change', (e) => {
const enabled = (e.target as HTMLInputElement).checked;
localStorage.setItem('fullWidthMode', enabled.toString());
applyFullWidthMode(enabled);
});
}
// Shortcuts UI Handlers
if (dom.openShortcutsBtn) {
dom.openShortcutsBtn.addEventListener('click', () => {
renderShortcutsList();
dom.shortcutsModal.classList.remove('hidden');
});
}
if (dom.closeShortcutsModalBtn) {
dom.closeShortcutsModalBtn.addEventListener('click', () => {
dom.shortcutsModal.classList.add('hidden');
});
}
// Close modal on outside click
if (dom.shortcutsModal) {
dom.shortcutsModal.addEventListener('click', (e) => {
if (e.target === dom.shortcutsModal) {
dom.shortcutsModal.classList.add('hidden');
}
});
}
if (dom.resetShortcutsBtn) {
dom.resetShortcutsBtn.addEventListener('click', async () => {
const confirmed = await showWarningModal(
'Reset Shortcuts',
'Are you sure you want to reset all shortcuts to default?<br><br>This action cannot be undone.',
true
);
if (confirmed) {
ShortcutsManager.reset();
renderShortcutsList();
}
});
}
if (dom.exportShortcutsBtn) {
dom.exportShortcutsBtn.addEventListener('click', () => {
ShortcutsManager.exportSettings();
});
}
if (dom.importShortcutsBtn) {
dom.importShortcutsBtn.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = async (e) => {
const content = e.target?.result as string;
if (ShortcutsManager.importSettings(content)) {
renderShortcutsList();
await showWarningModal(
'Import Successful',
'Shortcuts imported successfully!',
false
);
} else {
await showWarningModal(
'Import Failed',
'Failed to import shortcuts. Invalid file format.',
false
);
}
};
reader.readAsText(file);
}
};
input.click();
});
}
if (dom.shortcutSearch) {
dom.shortcutSearch.addEventListener('input', (e) => {
const term = (e.target as HTMLInputElement).value.toLowerCase();
const sections = dom.shortcutsList.querySelectorAll('.category-section');
sections.forEach((section) => {
const items = section.querySelectorAll('.shortcut-item');
let visibleCount = 0;
items.forEach((item) => {
const text = item.textContent?.toLowerCase() || '';
if (text.includes(term)) {
item.classList.remove('hidden');
visibleCount++;
} else {
item.classList.add('hidden');
}
});
if (visibleCount === 0) {
section.classList.add('hidden');
} else {
section.classList.remove('hidden');
}
});
});
}
// Reserved shortcuts that commonly conflict with browser/OS functions
const RESERVED_SHORTCUTS: Record<string, { mac?: string; windows?: string }> = {
'mod+w': { mac: 'Closes tab', windows: 'Closes tab' },
'mod+t': { mac: 'Opens new tab', windows: 'Opens new tab' },
'mod+n': { mac: 'Opens new window', windows: 'Opens new window' },
'mod+shift+n': { mac: 'Opens incognito window', windows: 'Opens incognito window' },
'mod+q': { mac: 'Quits application (cannot be overridden)' },
'mod+m': { mac: 'Minimizes window' },
'mod+h': { mac: 'Hides window' },
'mod+r': { mac: 'Reloads page', windows: 'Reloads page' },
'mod+shift+r': { mac: 'Hard reloads page', windows: 'Hard reloads page' },
'mod+l': { mac: 'Focuses address bar', windows: 'Focuses address bar' },
'mod+d': { mac: 'Bookmarks page', windows: 'Bookmarks page' },
'mod+shift+t': { mac: 'Reopens closed tab', windows: 'Reopens closed tab' },
'mod+shift+w': { mac: 'Closes window', windows: 'Closes window' },
'mod+tab': { mac: 'Switches tabs', windows: 'Switches apps' },
'alt+f4': { windows: 'Closes window' },
'ctrl+tab': { mac: 'Switches tabs', windows: 'Switches tabs' },
};
function getReservedShortcutWarning(combo: string, isMac: boolean): string | null {
const reserved = RESERVED_SHORTCUTS[combo];
if (!reserved) return null;
const description = isMac ? reserved.mac : reserved.windows;
if (!description) return null;
return description;
}
function showWarningModal(title: string, message: string, confirmMode: boolean = true): Promise<boolean> {
return new Promise((resolve) => {
if (!dom.warningModal || !dom.warningTitle || !dom.warningMessage || !dom.warningCancelBtn || !dom.warningConfirmBtn) {
resolve(confirmMode ? confirm(message) : (alert(message), true));
return;
}
dom.warningTitle.textContent = title;
dom.warningMessage.innerHTML = message;
dom.warningModal.classList.remove('hidden');
dom.warningModal.classList.add('flex');
if (confirmMode) {
dom.warningCancelBtn.style.display = '';
dom.warningConfirmBtn.textContent = 'Proceed';
} else {
dom.warningCancelBtn.style.display = 'none';
dom.warningConfirmBtn.textContent = 'OK';
}
const handleConfirm = () => {
cleanup();
resolve(true);
};
const handleCancel = () => {
cleanup();
resolve(false);
};
const cleanup = () => {
dom.warningModal?.classList.add('hidden');
dom.warningModal?.classList.remove('flex');
dom.warningConfirmBtn?.removeEventListener('click', handleConfirm);
dom.warningCancelBtn?.removeEventListener('click', handleCancel);
};
dom.warningConfirmBtn.addEventListener('click', handleConfirm);
dom.warningCancelBtn.addEventListener('click', handleCancel);
// Close on backdrop click
dom.warningModal.addEventListener('click', (e) => {
if (e.target === dom.warningModal) {
if (confirmMode) {
handleCancel();
} else {
handleConfirm();
}
}
}, { once: true });
});
}
function getToolId(tool: any): string {
if (tool.id) return tool.id;
if (tool.href) {
const match = tool.href.match(/\/([^/]+)\.html$/);
return match ? match[1] : tool.href;
}
return 'unknown';
}
function renderShortcutsList() {
if (!dom.shortcutsList) return;
dom.shortcutsList.innerHTML = '';
const allShortcuts = ShortcutsManager.getAllShortcuts();
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
const allTools = categories.flatMap(c => c.tools);
categories.forEach(category => {
const section = document.createElement('div');
section.className = 'category-section mb-6 last:mb-0';
const header = document.createElement('h3');
header.className = 'text-gray-400 text-xs font-bold uppercase tracking-wider mb-3 pl-1';
header.textContent = category.name;
section.appendChild(header);
const itemsContainer = document.createElement('div');
itemsContainer.className = 'space-y-2';
section.appendChild(itemsContainer);
let hasTools = false;
category.tools.forEach(tool => {
hasTools = true;
const toolId = getToolId(tool);
const currentShortcut = allShortcuts.get(toolId) || '';
const item = document.createElement('div');
item.className = 'shortcut-item flex items-center justify-between p-3 bg-gray-900 rounded-lg border border-gray-700 hover:border-gray-600 transition-colors';
const left = document.createElement('div');
left.className = 'flex items-center gap-3';
const icon = document.createElement('i');
icon.className = 'w-5 h-5 text-indigo-400';
icon.setAttribute('data-lucide', tool.icon);
const name = document.createElement('span');
name.className = 'text-gray-200 font-medium';
name.textContent = tool.name;
left.append(icon, name);
const right = document.createElement('div');
right.className = 'relative';
const input = document.createElement('input');
input.type = 'text';
input.className = 'shortcut-input w-32 bg-gray-800 border border-gray-600 text-white text-center text-sm rounded px-2 py-1 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all';
input.placeholder = 'Click to set';
input.value = formatShortcutDisplay(currentShortcut, isMac);
input.readOnly = true;
const clearBtn = document.createElement('button');
clearBtn.className = 'absolute -right-2 -top-2 bg-gray-700 hover:bg-red-600 text-white rounded-full p-0.5 hidden group-hover:block shadow-sm';
clearBtn.innerHTML = '<i data-lucide="x" class="w-3 h-3"></i>';
if (currentShortcut) {
right.classList.add('group');
}
clearBtn.onclick = (e) => {
e.stopPropagation();
ShortcutsManager.setShortcut(toolId, '');
renderShortcutsList();
};
input.onkeydown = async (e) => {
e.preventDefault();
e.stopPropagation();
if (e.key === 'Backspace' || e.key === 'Delete') {
ShortcutsManager.setShortcut(toolId, '');
renderShortcutsList();
return;
}
const keys: string[] = [];
// On Mac: metaKey = Command, ctrlKey = Control
// On Windows/Linux: metaKey is rare, ctrlKey = Ctrl
if (isMac) {
if (e.metaKey) keys.push('mod'); // Command on Mac
if (e.ctrlKey) keys.push('ctrl'); // Control on Mac (separate from Command)
} else {
if (e.ctrlKey || e.metaKey) keys.push('mod'); // Ctrl on Windows/Linux
}
if (e.altKey) keys.push('alt');
if (e.shiftKey) keys.push('shift');
let key = e.key.toLowerCase();
if (e.altKey && e.code) {
if (e.code.startsWith('Key')) {
key = e.code.slice(3).toLowerCase();
} else if (e.code.startsWith('Digit')) {
key = e.code.slice(5);
}
}
const isModifier = ['control', 'shift', 'alt', 'meta'].includes(key);
const isDeadKey = key === 'dead' || key.startsWith('dead');
// Ignore dead keys (used for accented characters on Mac with Option key)
if (isDeadKey) {
input.value = formatShortcutDisplay(ShortcutsManager.getShortcut(toolId) || '', isMac);
return;
}
if (!isModifier) {
keys.push(key);
}
const combo = keys.join('+');
input.value = formatShortcutDisplay(combo, isMac);
if (!isModifier) {
const existingToolId = ShortcutsManager.findToolByShortcut(combo);
if (existingToolId && existingToolId !== toolId) {
const existingTool = allTools.find(t => getToolId(t) === existingToolId);
const existingToolName = existingTool?.name || existingToolId;
const displayCombo = formatShortcutDisplay(combo, isMac);
await showWarningModal(
'Shortcut Already in Use',
`<strong>${displayCombo}</strong> is already assigned to:<br><br>` +
`<em>"${existingToolName}"</em><br><br>` +
`Please choose a different shortcut.`,
false
);
input.value = formatShortcutDisplay(ShortcutsManager.getShortcut(toolId) || '', isMac);
input.classList.remove('border-indigo-500', 'text-indigo-400');
input.blur();
return;
}
// Check if this is a reserved shortcut
const reservedWarning = getReservedShortcutWarning(combo, isMac);
if (reservedWarning) {
const displayCombo = formatShortcutDisplay(combo, isMac);
const shouldProceed = await showWarningModal(
'Reserved Shortcut Warning',
`<strong>${displayCombo}</strong> is commonly used for:<br><br>` +
`"<em>${reservedWarning}</em>"<br><br>` +
`This shortcut may not work reliably or might conflict with browser/system behavior.<br><br>` +
`Do you want to use it anyway?`
);
if (!shouldProceed) {
// Revert display
input.value = formatShortcutDisplay(ShortcutsManager.getShortcut(toolId) || '', isMac);
input.classList.remove('border-indigo-500', 'text-indigo-400');
input.blur();
return;
}
}
ShortcutsManager.setShortcut(toolId, combo);
// Re-render to update all inputs (show conflicts in real-time)
renderShortcutsList();
}
};
input.onkeyup = (e) => {
// If the user releases a modifier without pressing a main key, revert to saved
const key = e.key.toLowerCase();
if (['control', 'shift', 'alt', 'meta'].includes(key)) {
const currentSaved = ShortcutsManager.getShortcut(toolId);
}
};
input.onfocus = () => {
input.value = 'Press keys...';
input.classList.add('border-indigo-500', 'text-indigo-400');
};
input.onblur = () => {
input.value = formatShortcutDisplay(ShortcutsManager.getShortcut(toolId) || '', isMac);
input.classList.remove('border-indigo-500', 'text-indigo-400');
};
right.append(input);
if (currentShortcut) right.append(clearBtn);
item.append(left, right);
itemsContainer.appendChild(item);
});
if (hasTools) {
dom.shortcutsList.appendChild(section);
}
});
createIcons({ icons });
}
const scrollToTopBtn = document.getElementById('scroll-to-top-btn');
if (scrollToTopBtn) {
let lastScrollY = window.scrollY;
window.addEventListener('scroll', () => {
const currentScrollY = window.scrollY;
if (currentScrollY < lastScrollY && currentScrollY > 300) {
scrollToTopBtn.classList.add('visible');
} else {
scrollToTopBtn.classList.remove('visible');
}
lastScrollY = currentScrollY;
});
scrollToTopBtn.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'instant'
});
}); });
} }
}; };
+28 -170
View File
@@ -1,14 +1,8 @@
import { resetState } from './state.js'; import { resetState } from './state.js';
import { formatBytes, getPDFDocument } from './utils/helpers.js'; import { formatBytes } from './utils/helpers.js';
import { tesseractLanguages } from './config/tesseract-languages.js'; import { tesseractLanguages } from './config/tesseract-languages.js';
import { renderPagesProgressively, cleanupLazyRendering } from './utils/render-utils.js';
import { icons, createIcons } from 'lucide'; import { icons, createIcons } from 'lucide';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { getRotationState, updateRotationState } from './handlers/fileHandler.js';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
// Centralizing DOM element selection // Centralizing DOM element selection
export const dom = { export const dom = {
@@ -28,19 +22,6 @@ export const dom = {
toolsHeader: document.getElementById('tools-header'), toolsHeader: document.getElementById('tools-header'),
dividers: document.querySelectorAll('.section-divider'), dividers: document.querySelectorAll('.section-divider'),
hideSections: document.querySelectorAll('.hide-section'), hideSections: document.querySelectorAll('.hide-section'),
shortcutsModal: document.getElementById('shortcuts-modal'),
closeShortcutsModalBtn: document.getElementById('close-shortcuts-modal'),
shortcutsList: document.getElementById('shortcuts-list'),
shortcutSearch: document.getElementById('shortcut-search'),
resetShortcutsBtn: document.getElementById('reset-shortcuts-btn'),
importShortcutsBtn: document.getElementById('import-shortcuts-btn'),
exportShortcutsBtn: document.getElementById('export-shortcuts-btn'),
openShortcutsBtn: document.getElementById('open-shortcuts-btn'),
warningModal: document.getElementById('warning-modal'),
warningTitle: document.getElementById('warning-title'),
warningMessage: document.getElementById('warning-message'),
warningCancelBtn: document.getElementById('warning-cancel-btn'),
warningConfirmBtn: document.getElementById('warning-confirm-btn'),
}; };
export const showLoader = (text = 'Processing...') => { export const showLoader = (text = 'Processing...') => {
@@ -130,20 +111,25 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
if (!container) return; if (!container) return;
container.innerHTML = ''; container.innerHTML = '';
// Cleanup any previous lazy loading observers
cleanupLazyRendering();
showLoader('Rendering page previews...'); showLoader('Rendering page previews...');
const pdfData = await pdfDoc.save(); const pdfData = await pdfDoc.save();
const pdf = await getPDFDocument({ data: pdfData }).promise; // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
// Function to create wrapper element for each page
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail relative group';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'. // @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = pageNumber - 1; wrapper.dataset.pageIndex = i - 1;
const imgContainer = document.createElement('div'); const imgContainer = document.createElement('div');
imgContainer.className = imgContainer.className =
@@ -162,7 +148,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
const pageNumSpan = document.createElement('span'); const pageNumSpan = document.createElement('span');
pageNumSpan.className = pageNumSpan.className =
'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1'; 'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
pageNumSpan.textContent = pageNumber.toString(); pageNumSpan.textContent = i.toString();
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.className = deleteBtn.className =
@@ -170,36 +156,14 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
deleteBtn.innerHTML = '&times;'; deleteBtn.innerHTML = '&times;';
deleteBtn.addEventListener('click', (e) => { deleteBtn.addEventListener('click', (e) => {
(e.currentTarget as HTMLElement).parentElement.remove(); (e.currentTarget as HTMLElement).parentElement.remove();
// Renumber remaining pages
const pages = container.querySelectorAll('.page-thumbnail');
pages.forEach((page, index) => {
const numSpan = page.querySelector('span');
if (numSpan) {
numSpan.textContent = (index + 1).toString();
}
});
initializeOrganizeSortable(containerId); initializeOrganizeSortable(containerId);
}); });
wrapper.append(pageNumSpan, deleteBtn); wrapper.append(pageNumSpan, deleteBtn);
} else if (toolId === 'rotate') { } else if (toolId === 'rotate') {
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2'; wrapper.className = 'page-rotator-item flex flex-col items-center gap-2';
wrapper.dataset.rotation = '0';
// Read rotation from state (handles "Rotate All" on lazy-loaded pages)
const rotationStateArray = getRotationState();
const pageIndex = pageNumber - 1;
const initialRotation = rotationStateArray[pageIndex] || 0;
wrapper.dataset.rotation = initialRotation.toString();
img.classList.add('transition-transform', 'duration-300'); img.classList.add('transition-transform', 'duration-300');
// Apply initial rotation if any
if (initialRotation !== 0) {
img.style.transform = `rotate(${initialRotation}deg)`;
}
wrapper.appendChild(imgContainer); wrapper.appendChild(imgContainer);
const controlsDiv = document.createElement('div'); const controlsDiv = document.createElement('div');
@@ -207,7 +171,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
const pageNumSpan = document.createElement('span'); const pageNumSpan = document.createElement('span');
pageNumSpan.className = 'font-medium text-sm text-white'; pageNumSpan.className = 'font-medium text-sm text-white';
pageNumSpan.textContent = pageNumber.toString(); pageNumSpan.textContent = i.toString();
const rotateBtn = document.createElement('button'); const rotateBtn = document.createElement('button');
rotateBtn.className = rotateBtn.className =
@@ -220,53 +184,25 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
'.page-rotator-item' '.page-rotator-item'
) as HTMLElement; ) as HTMLElement;
const imgEl = card.querySelector('img'); const imgEl = card.querySelector('img');
const pageIndex = pageNumber - 1;
let currentRotation = parseInt(card.dataset.rotation); let currentRotation = parseInt(card.dataset.rotation);
currentRotation = (currentRotation + 90) % 360; currentRotation = (currentRotation + 90) % 360;
card.dataset.rotation = currentRotation.toString(); card.dataset.rotation = currentRotation.toString();
imgEl.style.transform = `rotate(${currentRotation}deg)`; imgEl.style.transform = `rotate(${currentRotation}deg)`;
updateRotationState(pageIndex, currentRotation);
}); });
controlsDiv.append(pageNumSpan, rotateBtn); controlsDiv.append(pageNumSpan, rotateBtn);
wrapper.appendChild(controlsDiv); wrapper.appendChild(controlsDiv);
} }
return wrapper; container.appendChild(wrapper);
};
try {
// Render pages progressively with lazy loading
await renderPagesProgressively(
pdf,
container,
createWrapper,
{
batchSize: 6,
useLazyLoading: true,
lazyLoadMargin: '300px',
onProgress: (current, total) => {
showLoader(`Rendering page previews: ${current}/${total}`);
},
onBatchComplete: () => {
createIcons({ icons }); createIcons({ icons });
} }
}
);
if (toolId === 'organize') { if (toolId === 'organize') {
initializeOrganizeSortable(containerId); initializeOrganizeSortable(containerId);
} }
// Reinitialize lucide icons for dynamically added elements
createIcons({ icons });
} catch (error) {
console.error('Error rendering page thumbnails:', error);
showAlert('Error', 'Failed to render page thumbnails');
} finally {
hideLoader(); hideLoader();
}
}; };
/** /**
@@ -315,7 +251,8 @@ const createFileInputHTML = (options = {}) => {
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}"> <input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}">
</div> </div>
${showControls ${
showControls
? ` ? `
<!-- NEW: Add control buttons for multi-file uploads --> <!-- NEW: Add control buttons for multi-file uploads -->
<div id="file-controls" class="hidden mt-4 flex gap-3"> <div id="file-controls" class="hidden mt-4 flex gap-3">
@@ -636,8 +573,8 @@ export const toolTemplates = {
<div class="mb-4"> <div class="mb-4">
<label for="jpg-quality" class="block mb-2 text-sm font-medium text-gray-300">Image Quality</label> <label for="jpg-quality" class="block mb-2 text-sm font-medium text-gray-300">Image Quality</label>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<input type="range" id="jpg-quality" min="0.1" max="1.0" step="0.01" value="1.0" class="flex-1"> <input type="range" id="jpg-quality" min="0.1" max="1.0" step="0.1" value="0.9" class="flex-1">
<span id="jpg-quality-value" class="text-white font-medium w-16 text-right">100%</span> <span id="jpg-quality-value" class="text-white font-medium w-16 text-right">90%</span>
</div> </div>
<p class="mt-1 text-xs text-gray-400">Higher quality = larger file size</p> <p class="mt-1 text-xs text-gray-400">Higher quality = larger file size</p>
</div> </div>
@@ -814,7 +751,7 @@ export const toolTemplates = {
flatten: () => ` flatten: () => `
<h2 class="text-2xl font-bold text-white mb-4">Flatten PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">Flatten PDF</h2>
<p class="mb-6 text-gray-400">Make PDF forms and annotations non-editable by flattening them.</p> <p class="mb-6 text-gray-400">Make PDF forms and annotations non-editable by flattening them.</p>
${createFileInputHTML({ multiple: true, showControls: true })} ${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="hidden mt-6 btn-gradient w-full">Flatten PDF</button> <button id="process-btn" class="hidden mt-6 btn-gradient w-full">Flatten PDF</button>
`, `,
@@ -1051,14 +988,8 @@ export const toolTemplates = {
'image-to-pdf': () => ` 'image-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2> <h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2>
<p class="mb-4 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p> <p class="mb-6 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p>
${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp', showControls: true })}
<div class="mb-6 p-3 bg-gray-900/50 border border-gray-700 rounded-lg">
<p class="text-sm text-gray-300 mb-2"><strong class="text-white">Supported Formats:</strong></p>
<p class="text-xs text-gray-400">JPG, PNG, WebP, BMP, TIFF, SVG, HEIC/HEIF</p>
</div>
${createFileInputHTML({ multiple: true, accept: 'image/jpeg,image/png,image/webp,image/bmp,image/tiff,image/svg+xml', showControls: true })}
<ul id="image-list" class="mt-4 grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4"> <ul id="image-list" class="mt-4 grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4">
</ul> </ul>
<div id="image-to-pdf-options" class="hidden mt-6"> <div id="image-to-pdf-options" class="hidden mt-6">
@@ -1320,13 +1251,8 @@ export const toolTemplates = {
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="dimensions-results" class="hidden mt-6"> <div id="dimensions-results" class="hidden mt-6">
<!-- Summary Statistics Panel --> <div class="flex justify-end mb-4">
<div id="dimensions-summary" class="mb-6"></div> <label for="units-select" class="text-sm font-medium text-gray-300 self-center mr-3">Display Units:</label>
<!-- Controls Row -->
<div class="flex flex-wrap justify-between items-center gap-4 mb-4">
<div class="flex items-center gap-3">
<label for="units-select" class="text-sm font-medium text-gray-300">Display Units:</label>
<select id="units-select" class="bg-gray-700 border border-gray-600 text-white rounded-lg p-2"> <select id="units-select" class="bg-gray-700 border border-gray-600 text-white rounded-lg p-2">
<option value="pt" selected>Points (pt)</option> <option value="pt" selected>Points (pt)</option>
<option value="in">Inches (in)</option> <option value="in">Inches (in)</option>
@@ -1334,13 +1260,6 @@ export const toolTemplates = {
<option value="px">Pixels (at 96 DPI)</option> <option value="px">Pixels (at 96 DPI)</option>
</select> </select>
</div> </div>
<button id="export-csv-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
<i data-lucide="download" class="w-4 h-4"></i>
Export to CSV
</button>
</div>
<!-- Dimensions Table -->
<div class="overflow-x-auto rounded-lg border border-gray-700"> <div class="overflow-x-auto rounded-lg border border-gray-700">
<table class="min-w-full divide-y divide-gray-700 text-sm text-left"> <table class="min-w-full divide-y divide-gray-700 text-sm text-left">
<thead class="bg-gray-900"> <thead class="bg-gray-900">
@@ -1349,9 +1268,6 @@ export const toolTemplates = {
<th class="px-4 py-3 font-medium text-white">Dimensions (W x H)</th> <th class="px-4 py-3 font-medium text-white">Dimensions (W x H)</th>
<th class="px-4 py-3 font-medium text-white">Standard Size</th> <th class="px-4 py-3 font-medium text-white">Standard Size</th>
<th class="px-4 py-3 font-medium text-white">Orientation</th> <th class="px-4 py-3 font-medium text-white">Orientation</th>
<th class="px-4 py-3 font-medium text-white">Aspect Ratio</th>
<th class="px-4 py-3 font-medium text-white">Area</th>
<th class="px-4 py-3 font-medium text-white">Rotation</th>
</tr> </tr>
</thead> </thead>
<tbody id="dimensions-table-body" class="divide-y divide-gray-700"> <tbody id="dimensions-table-body" class="divide-y divide-gray-700">
@@ -1361,7 +1277,6 @@ export const toolTemplates = {
</div> </div>
`, `,
'n-up': () => ` 'n-up': () => `
<h2 class="text-2xl font-bold text-white mb-4">N-Up Page Arrangement</h2> <h2 class="text-2xl font-bold text-white mb-4">N-Up Page Arrangement</h2>
<p class="mb-6 text-gray-400">Combine multiple pages from your PDF onto a single sheet. This is great for creating booklets or proof sheets.</p> <p class="mb-6 text-gray-400">Combine multiple pages from your PDF onto a single sheet. This is great for creating booklets or proof sheets.</p>
@@ -1440,19 +1355,11 @@ export const toolTemplates = {
'combine-single-page': () => ` 'combine-single-page': () => `
<h2 class="text-2xl font-bold text-white mb-4">Combine to a Single Page</h2> <h2 class="text-2xl font-bold text-white mb-4">Combine to a Single Page</h2>
<p class="mb-6 text-gray-400">Stitch all pages of your PDF together vertically or horizontally to create one continuous page.</p> <p class="mb-6 text-gray-400">Stitch all pages of your PDF together vertically to create one continuous, scrollable page.</p>
${createFileInputHTML()} ${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="combine-options" class="hidden mt-6 space-y-4"> <div id="combine-options" class="hidden mt-6 space-y-4">
<div>
<label for="combine-orientation" class="block mb-2 text-sm font-medium text-gray-300">Orientation</label>
<select id="combine-orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="vertical" selected>Vertical (Stack pages top to bottom)</option>
<option value="horizontal">Horizontal (Stack pages left to right)</option>
</select>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label for="page-spacing" class="block mb-2 text-sm font-medium text-gray-300">Spacing Between Pages (in points)</label> <label for="page-spacing" class="block mb-2 text-sm font-medium text-gray-300">Spacing Between Pages (in points)</label>
@@ -1463,25 +1370,12 @@ export const toolTemplates = {
<input type="color" id="background-color" value="#FFFFFF" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer"> <input type="color" id="background-color" value="#FFFFFF" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer">
</div> </div>
</div> </div>
<div> <div>
<label class="flex items-center gap-2 text-sm font-medium text-gray-300"> <label class="flex items-center gap-2 text-sm font-medium text-gray-300">
<input type="checkbox" id="add-separator" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500"> <input type="checkbox" id="add-separator" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
Draw a separator line between pages Draw a separator line between pages
</label> </label>
</div> </div>
<div id="separator-options" class="hidden grid grid-cols-1 sm:grid-cols-2 gap-4 p-4 rounded-lg bg-gray-900 border border-gray-700">
<div>
<label for="separator-thickness" class="block mb-2 text-sm font-medium text-gray-300">Separator Line Thickness (in points)</label>
<input type="number" id="separator-thickness" value="0.5" min="0.1" max="10" step="0.1" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
</div>
<div>
<label for="separator-color" class="block mb-2 text-sm font-medium text-gray-300">Separator Line Color</label>
<input type="color" id="separator-color" value="#CCCCCC" class="w-full h-[42px] bg-gray-700 border border-gray-600 rounded-lg p-1 cursor-pointer">
</div>
</div>
<button id="process-btn" class="btn-gradient w-full mt-6">Combine Pages</button> <button id="process-btn" class="btn-gradient w-full mt-6">Combine Pages</button>
</div> </div>
`, `,
@@ -1790,14 +1684,6 @@ export const toolTemplates = {
<div id="canvas-container-sign" class="relative w-full overflow-auto bg-gray-900 rounded-lg border border-gray-600" style="height: 85vh;"> <div id="canvas-container-sign" class="relative w-full overflow-auto bg-gray-900 rounded-lg border border-gray-600" style="height: 85vh;">
<!-- PDF.js viewer iframe will be loaded here --> <!-- PDF.js viewer iframe will be loaded here -->
</div> </div>
<div class="mt-4 flex items-center gap-2">
<label class="flex items-center gap-2 text-sm font-medium text-gray-300 cursor-pointer">
<input type="checkbox" id="flatten-signature-toggle" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
Flatten PDF (use the Save button below)
</label>
</div>
<button id="process-btn" class="btn-gradient w-full mt-4" style="display:none;">Save & Download Signed PDF</button> <button id="process-btn" class="btn-gradient w-full mt-4" style="display:none;">Save & Download Signed PDF</button>
</div> </div>
`, `,
@@ -2098,34 +1984,6 @@ export const toolTemplates = {
<div id="attachment-file-list" class="mt-4 space-y-2"></div> <div id="attachment-file-list" class="mt-4 space-y-2"></div>
<div id="attachment-level-options" class="hidden mt-6 space-y-4">
<div>
<h3 class="text-lg font-semibold text-white mb-2">Attachment Level</h3>
<div class="flex gap-4 p-2 rounded-lg bg-gray-900">
<label class="flex-1 flex items-center gap-2 p-3 rounded-md hover:bg-gray-700 cursor-pointer">
<input type="radio" name="attachment-level" value="document" checked class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
<div>
<span class="font-semibold text-white">Document Level</span>
<p class="text-xs text-gray-400">Attach to the entire document</p>
</div>
</label>
<label class="flex-1 flex items-center gap-2 p-3 rounded-md hover:bg-gray-700 cursor-pointer">
<input type="radio" name="attachment-level" value="page" class="w-4 h-4 text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
<div>
<span class="font-semibold text-white">Page Level</span>
<p class="text-xs text-gray-400">Attach to specific pages</p>
</div>
</label>
</div>
</div>
<div id="page-range-wrapper" class="hidden">
<label for="attachment-page-range" class="block mb-2 text-sm font-medium text-gray-300">Page Range</label>
<input type="text" id="attachment-page-range" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="e.g., 1-3, 5, 8">
<p class="text-xs text-gray-400 mt-1">Attachments will be added to each page in this range. Total Pages: <span id="attachment-total-pages"></span></p>
</div>
</div>
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button> <button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button>
</div> </div>
`, `,
-34
View File
@@ -1,34 +0,0 @@
// Full-width mode utility
// This script applies the full-width preference from localStorage to page uploaders
export function initFullWidthMode() {
const savedFullWidth = localStorage.getItem('fullWidthMode') === 'true';
if (savedFullWidth) {
applyFullWidthMode(true);
}
}
export function applyFullWidthMode(enabled: boolean) {
// Apply to all page uploaders
const pageUploaders = document.querySelectorAll('#tool-uploader');
pageUploaders.forEach((uploader) => {
if (enabled) {
uploader.classList.remove('max-w-2xl', 'max-w-5xl');
} else {
// Restore original max-width if not already present
if (!uploader.classList.contains('max-w-2xl') && !uploader.classList.contains('max-w-5xl')) {
uploader.classList.add('max-w-2xl');
}
}
});
}
// Auto-initialize on DOM load
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initFullWidthMode);
} else {
initFullWidthMode();
}
}
+5 -93
View File
@@ -1,9 +1,6 @@
import createModule from '@neslinesli93/qpdf-wasm'; import createModule from '@neslinesli93/qpdf-wasm';
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui';
import { createIcons } from 'lucide'; import { createIcons } from 'lucide';
import { state, resetState } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist'
const STANDARD_SIZES = { const STANDARD_SIZES = {
A4: { width: 595.28, height: 841.89 }, A4: { width: 595.28, height: 841.89 },
@@ -48,17 +45,16 @@ export function convertPoints(points: any, unit: any) {
return result.toFixed(2); return result.toFixed(2);
} }
// Convert hex color to RGB export const hexToRgb = (hex: any) => {
export function hexToRgb(hex: string): { r: number; g: number; b: number } { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result return result
? { ? {
r: parseInt(result[1], 16) / 255, r: parseInt(result[1], 16) / 255,
g: parseInt(result[2], 16) / 255, g: parseInt(result[2], 16) / 255,
b: parseInt(result[3], 16) / 255, b: parseInt(result[3], 16) / 255,
} }
: { r: 0, g: 0, b: 0 } : { r: 0, g: 0, b: 0 }; // Default to black
} };
export const formatBytes = (bytes: any, decimals = 1) => { export const formatBytes = (bytes: any, decimals = 1) => {
if (bytes === 0) return '0 Bytes'; if (bytes === 0) return '0 Bytes';
@@ -198,87 +194,3 @@ export function formatStars(num: number) {
} }
return num.toLocaleString(); return num.toLocaleString();
}; };
/**
* Truncates a filename to a maximum length, adding ellipsis if needed.
* Preserves the file extension.
* @param filename - The filename to truncate
* @param maxLength - Maximum length (default: 30)
* @returns Truncated filename with ellipsis if needed
*/
export function truncateFilename(filename: string, maxLength: number = 25): string {
if (filename.length <= maxLength) {
return filename;
}
const lastDotIndex = filename.lastIndexOf('.');
const extension = lastDotIndex !== -1 ? filename.substring(lastDotIndex) : '';
const nameWithoutExt = lastDotIndex !== -1 ? filename.substring(0, lastDotIndex) : filename;
const availableLength = maxLength - extension.length - 3; // 3 for '...'
if (availableLength <= 0) {
return filename.substring(0, maxLength - 3) + '...';
}
return nameWithoutExt.substring(0, availableLength) + '...' + extension;
}
export function formatShortcutDisplay(shortcut: string, isMac: boolean): string {
if (!shortcut) return '';
return shortcut
.replace('mod', isMac ? '⌘' : 'Ctrl')
.replace('ctrl', isMac ? '^' : 'Ctrl') // Control key on Mac shows as ^
.replace('alt', isMac ? '⌥' : 'Alt')
.replace('shift', 'Shift')
.split('+')
.map(k => k.charAt(0).toUpperCase() + k.slice(1))
.join(isMac ? '' : '+');
}
export function resetAndReloadTool(preResetCallback?: () => void) {
const toolid = state.activeTool;
if (preResetCallback) {
preResetCallback();
}
resetState();
if (toolid) {
const element = document.querySelector(
`[data-tool-id="${toolid}"]`
) as HTMLElement;
if (element) element.click();
}
}
/**
* Wrapper for pdfjsLib.getDocument that adds the required wasmUrl configuration.
* Use this instead of calling pdfjsLib.getDocument directly.
* @param src The source to load (url string, typed array, or parameters object)
* @returns The PDF loading task
*/
export function getPDFDocument(src: any) {
let params = src;
// Handle different input types similar to how getDocument handles them,
// but we ensure we have an object to attach wasmUrl to.
if (typeof src === 'string') {
params = { url: src };
} else if (src instanceof Uint8Array || src instanceof ArrayBuffer) {
params = { data: src };
}
// Ensure params is an object
if (typeof params !== 'object' || params === null) {
params = {};
}
// Add wasmUrl pointing to our public/wasm directory
// This is required for PDF.js v5+ to load OpenJPEG for certain images
return pdfjsLib.getDocument({
...params,
wasmUrl: '/pdfjs-viewer/wasm/',
});
}
-381
View File
@@ -1,381 +0,0 @@
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
/**
* Configuration for progressive rendering
*/
export interface RenderConfig {
batchSize?: number;
useLazyLoading?: boolean;
lazyLoadMargin?: string;
eagerLoadBatches?: number; // Number of batches to load ahead eagerly (default: 1)
onProgress?: (current: number, total: number) => void;
onPageRendered?: (pageIndex: number, element: HTMLElement) => void;
onBatchComplete?: () => void;
shouldCancel?: () => boolean;
}
/**
* Page rendering task
*/
interface PageTask {
pageNumber: number;
pdfjsDoc: any;
fileName?: string;
container: HTMLElement;
scale?: number;
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement;
}
/**
* Lazy loading state
*/
interface LazyLoadState {
observer: IntersectionObserver | null;
pendingTasks: Map<HTMLElement, PageTask>;
isRendering: boolean;
eagerLoadQueue: PageTask[];
nextEagerIndex: number;
}
const lazyLoadState: LazyLoadState = {
observer: null,
pendingTasks: new Map(),
isRendering: false,
eagerLoadQueue: [],
nextEagerIndex: 0,
};
/**
* Creates a placeholder element for a page that will be lazy-loaded
*/
export function createPlaceholder(pageNumber: number, fileName?: string): HTMLElement {
const placeholder = document.createElement('div');
placeholder.className =
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 rounded-lg bg-gray-800 transition-colors';
placeholder.dataset.pageNumber = pageNumber.toString();
if (fileName) {
placeholder.dataset.fileName = fileName;
}
placeholder.dataset.lazyLoad = 'true';
// Create skeleton loader
const skeletonContainer = document.createElement('div');
skeletonContainer.className = 'relative w-full h-36 bg-gray-700 rounded-md animate-pulse flex items-center justify-center';
const loadingText = document.createElement('span');
loadingText.className = 'text-gray-500 text-xs';
loadingText.textContent = 'Loading...';
skeletonContainer.appendChild(loadingText);
placeholder.appendChild(skeletonContainer);
return placeholder;
}
/**
* Renders a single page to canvas
*/
export async function renderPageToCanvas(
pdfjsDoc: any,
pageNumber: number,
scale: number = 0.5
): Promise<HTMLCanvasElement> {
const page = await pdfjsDoc.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
canvas: canvas,
viewport,
}).promise;
return canvas;
}
/**
* Renders a batch of pages in parallel
*/
async function renderPageBatch(
tasks: PageTask[],
onProgress?: (current: number, total: number) => void
): Promise<void> {
const renderPromises = tasks.map(async (task) => {
try {
const canvas = await renderPageToCanvas(
task.pdfjsDoc,
task.pageNumber,
task.scale || 0.5
);
const wrapper = task.createWrapper(canvas, task.pageNumber, task.fileName);
// Find and replace the placeholder for this specific page number
const placeholder = task.container.querySelector(
`[data-page-number="${task.pageNumber}"][data-lazy-load="true"]`
);
if (placeholder) {
// Replace placeholder with rendered page
task.container.replaceChild(wrapper, placeholder);
} else {
// Fallback: shouldn't happen with new approach, but just in case
console.warn(`No placeholder found for page ${task.pageNumber}, appending instead`);
task.container.appendChild(wrapper);
}
return wrapper;
} catch (error) {
console.error(`Error rendering page ${task.pageNumber}:`, error);
return null;
}
});
await Promise.all(renderPromises);
}
/**
* Sets up Intersection Observer for lazy loading
*/
function setupLazyRendering(
container: HTMLElement,
config: RenderConfig
): IntersectionObserver {
const options = {
root: container.closest('.overflow-auto') || null,
rootMargin: config.lazyLoadMargin || '200px',
threshold: 0.01,
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const placeholder = entry.target as HTMLElement;
const task = lazyLoadState.pendingTasks.get(placeholder);
if (task) {
// Immediately unobserve to prevent multiple triggers
observer.unobserve(placeholder);
lazyLoadState.pendingTasks.delete(placeholder);
// Render this page immediately (not waiting for isRendering flag)
renderPageBatch([task], config.onProgress)
.then(() => {
// Trigger callback after lazy load batch
if (config.onBatchComplete) {
config.onBatchComplete();
}
// Check if all pages are rendered
if (lazyLoadState.pendingTasks.size === 0 && lazyLoadState.observer) {
lazyLoadState.observer.disconnect();
lazyLoadState.observer = null;
}
})
.catch((error) => {
console.error(`Error lazy loading page ${task.pageNumber}:`, error);
});
}
}
});
}, options);
lazyLoadState.observer = observer;
return observer;
}
/**
* Request idle callback with fallback
*/
function requestIdleCallbackPolyfill(callback: () => void): void {
if ('requestIdleCallback' in window) {
requestIdleCallback(callback);
} else {
setTimeout(callback, 16); // ~60fps
}
}
/**
* Main function to render pages progressively with optional lazy loading
*/
export async function renderPagesProgressively(
pdfjsDoc: any,
container: HTMLElement,
createWrapper: (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => HTMLElement,
config: RenderConfig = {}
): Promise<void> {
const {
batchSize = 8, // Increased from 5 to 8 for faster initial render
useLazyLoading = true,
eagerLoadBatches = 1, // Eagerly load 1 batch ahead by default
onProgress,
onBatchComplete,
} = config;
const totalPages = pdfjsDoc.numPages;
// Render more pages initially to reduce lazy loading issues
const initialRenderCount = useLazyLoading
? Math.min(20, totalPages) // Increased from 12 to 20 pages
: totalPages;
// CRITICAL FIX: Create placeholders for ALL pages first to maintain order
const placeholders: HTMLElement[] = [];
for (let i = 1; i <= totalPages; i++) {
const placeholder = createPlaceholder(i);
container.appendChild(placeholder);
placeholders.push(placeholder);
}
const tasks: PageTask[] = [];
// Create tasks for all pages
for (let i = 1; i <= totalPages; i++) {
tasks.push({
pageNumber: i,
pdfjsDoc,
container,
scale: config.useLazyLoading ? 0.3 : 0.5,
createWrapper,
});
}
// If lazy loading is enabled, set up observer for pages beyond initial render
if (useLazyLoading && totalPages > initialRenderCount) {
const observer = setupLazyRendering(container, config);
for (let i = initialRenderCount + 1; i <= totalPages; i++) {
const placeholder = placeholders[i - 1];
// Store the task for lazy rendering
lazyLoadState.pendingTasks.set(placeholder, tasks[i - 1]);
observer.observe(placeholder);
}
// Prepare eager load queue
const eagerStartIndex = initialRenderCount;
const eagerEndIndex = Math.min(
eagerStartIndex + (eagerLoadBatches * batchSize),
totalPages
);
lazyLoadState.eagerLoadQueue = tasks.slice(eagerStartIndex, eagerEndIndex);
lazyLoadState.nextEagerIndex = 0;
}
// Render initial pages in batches
const initialTasks = tasks.slice(0, initialRenderCount);
for (let i = 0; i < initialTasks.length; i += batchSize) {
if (config.shouldCancel?.()) return;
const batch = initialTasks.slice(i, i + batchSize);
await new Promise<void>((resolve) => {
requestIdleCallbackPolyfill(async () => {
await renderPageBatch(batch, onProgress);
if (onProgress) {
onProgress(Math.min(i + batchSize, initialRenderCount), totalPages);
}
if (onBatchComplete) {
onBatchComplete();
}
resolve();
});
});
}
// Start eager loading AFTER initial batch is complete
if (useLazyLoading && eagerLoadBatches > 0 && totalPages > initialRenderCount) {
renderEagerBatch(config);
}
}
/**
* Manually observe a placeholder element (useful for dynamically created placeholders)
*/
export function observePlaceholder(
placeholder: HTMLElement,
task: PageTask
): void {
if (!lazyLoadState.observer) {
console.warn('No active observer to register placeholder');
return;
}
lazyLoadState.pendingTasks.set(placeholder, task);
lazyLoadState.observer.observe(placeholder);
}
/**
* Eagerly renders the next batch in the background
*/
function renderEagerBatch(config: RenderConfig): void {
const { eagerLoadBatches = 1, batchSize = 8 } = config;
if (eagerLoadBatches <= 0 || lazyLoadState.eagerLoadQueue.length === 0) {
return;
}
if (config.shouldCancel?.()) return;
const { nextEagerIndex, eagerLoadQueue } = lazyLoadState;
if (nextEagerIndex >= eagerLoadQueue.length) {
return; // All eager batches rendered
}
const batchEnd = Math.min(nextEagerIndex + batchSize, eagerLoadQueue.length);
const batch = eagerLoadQueue.slice(nextEagerIndex, batchEnd);
requestIdleCallbackPolyfill(async () => {
if (config.shouldCancel?.()) return;
// Remove these tasks from pending since we're rendering them eagerly
batch.forEach(task => {
const placeholder = Array.from(lazyLoadState.pendingTasks.entries())
.find(([_, t]) => t.pageNumber === task.pageNumber)?.[0];
if (placeholder && lazyLoadState.observer) {
lazyLoadState.observer.unobserve(placeholder);
lazyLoadState.pendingTasks.delete(placeholder);
}
});
await renderPageBatch(batch, config.onProgress);
if (config.onBatchComplete) {
config.onBatchComplete();
}
// Update next eager index
lazyLoadState.nextEagerIndex = batchEnd;
// Queue next eager batch
const remainingBatches = Math.ceil((eagerLoadQueue.length - batchEnd) / batchSize);
if (remainingBatches > 0 && remainingBatches < eagerLoadBatches) {
// Continue eager loading if we have more batches within the eager threshold
renderEagerBatch(config);
}
});
}
/**
* Cleanup function to disconnect observers
*/
export function cleanupLazyRendering(): void {
if (lazyLoadState.observer) {
lazyLoadState.observer.disconnect();
lazyLoadState.observer = null;
}
lazyLoadState.pendingTasks.clear();
lazyLoadState.isRendering = false;
lazyLoadState.eagerLoadQueue = [];
lazyLoadState.nextEagerIndex = 0;
}
-7
View File
@@ -1,7 +0,0 @@
import { ShortcutsManager } from '../logic/shortcuts.js';
export function initializeGlobalShortcuts() {
ShortcutsManager.init();
console.log('Global shortcuts initialized');
}
-27
View File
@@ -1,27 +0,0 @@
import { APP_VERSION } from '../../version.js';
// Handle simple mode footer replacement for tool pages
if (__SIMPLE_MODE__) {
const footer = document.querySelector('footer');
if (footer) {
footer.style.display = 'none';
const simpleFooter = document.createElement('footer');
simpleFooter.className = 'mt-16 border-t-2 border-gray-700 py-8';
simpleFooter.innerHTML = `
<div class="container mx-auto px-4">
<div class="flex items-center mb-4">
<img src="../../images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
<span class="text-white font-bold text-lg">BentoPDF</span>
</div>
<p class="text-gray-400 text-sm">
&copy; 2025 BentoPDF. All rights reserved.
</p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version-simple">${APP_VERSION}</span>
</p>
</div>
`;
document.body.appendChild(simpleFooter);
}
}
+7 -13
View File
@@ -59,8 +59,7 @@
</nav> </nav>
<div class="min-h-screen flex items-center justify-center p-4 bg-gray-900"> <div class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
<div id="tool-uploader" <div class="bg-gray-800 rounded-xl shadow-xl p-6 max-w-5xl w-full text-gray-200 border border-gray-700">
class="bg-gray-800 rounded-xl shadow-xl p-6 max-w-5xl w-full text-gray-200 border border-gray-700">
<button id="back-to-tools" <button id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"> class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i> <i data-lucide="arrow-left" class="cursor-pointer"></i>
@@ -99,13 +98,14 @@
</div> </div>
</div> </div>
<button id="save-stamped-btn" class="btn-gradient w-full mt-6 flex items-center justify-center gap-2"> <button id="save-stamped-btn"
class="btn-gradient w-full mt-6 flex items-center justify-center gap-2">
<span>Save Stamped PDF</span> <span>Save Stamped PDF</span>
</button> </button>
</div> </div>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8"> <footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div 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="mb-8 md:mb-0">
@@ -117,7 +117,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -139,9 +139,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -162,11 +159,11 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" /> d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.19 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
</svg> </svg>
</a> </a>
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400" <a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
@@ -189,9 +186,6 @@
</div> </div>
</footer> </footer>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../version.ts"></script>
<script type="module" src="../js/logic/add-stamps.ts"></script> <script type="module" src="../js/logic/add-stamps.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
+10 -18
View File
@@ -61,16 +61,14 @@
</div> </div>
</nav> </nav>
<div id="uploader" class="min-h-screen flex items-center justify-center p-4 bg-gray-900"> <div id="uploader" class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
<div id="tool-uploader" <div class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-md w-full text-gray-200 border border-gray-700">
class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
<button id="back-to-tools" <button id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"> class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i> <i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer"> Back to Tools </span> <span class="cursor-pointer"> Back to Tools </span>
</button> </button>
<h1 class="text-2xl font-bold text-white mb-2">Edit Bookmarks</h1>
<p class="text-gray-400 mb-6"> <p class="text-gray-400 mb-6">
Add, edit, import, delete and extract PDF bookmarks. Upload a PDF to begin editing bookmarks
</p> </p>
<!-- Drop Zone for Main PDF Upload --> <!-- Drop Zone for Main PDF Upload -->
@@ -415,14 +413,14 @@
<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="mb-8 md:mb-0">
<div class="flex items-center justify-center md:justify-start mb-4"> <div class="flex items-center justify-center md:justify-start mb-4">
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" /> <img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
<span class="text-xl font-bold text-white">BentoPDF</span> <span class="text-xl font-bold text-white">BentoPDF</span>
</div> </div>
<p class="text-gray-400 text-sm"> <p class="text-gray-400 text-sm">
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -430,13 +428,13 @@
<h3 class="font-bold text-white mb-4">Company</h3> <h3 class="font-bold text-white mb-4">Company</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="../../about.html" class="hover:text-indigo-400">About Us</a> <a href="./about.html" class="hover:text-indigo-400">About Us</a>
</li> </li>
<li> <li>
<a href="../../faq.html" class="hover:text-indigo-400">FAQ</a> <a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
</li> </li>
<li> <li>
<a href="../../contact.html" class="hover:text-indigo-400">Contact Us</a> <a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -445,13 +443,10 @@
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a> <a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
</li> </li>
<li> <li>
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a> <a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
</li>
<li>
<a href="../../privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -467,7 +462,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -494,9 +489,6 @@
</div> </div>
</footer> </footer>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../version.ts"></script>
<script type="module" src="../js/logic/bookmark-pdf.ts"></script> <script type="module" src="../js/logic/bookmark-pdf.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
-408
View File
@@ -1,408 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Create PDF Form - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
<link href="../../src/css/styles.css" rel="stylesheet" />
</head>
<body class="antialiased bg-gray-900">
<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="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
<span class="text-white font-bold text-xl ml-2">
<a href="/">BentoPDF</a>
</span>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8 text-white">
<a href="/" class="nav-link">Home</a>
<a href="/about.html" class="nav-link">About</a>
<a href="/contact.html" class="nav-link">Contact</a>
<a href="/" class="nav-link">All Tools</a>
</div>
<!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center">
<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">
<span class="sr-only">Open main menu</span>
<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>
<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>
</div>
</div>
<!-- Mobile Menu Dropdown -->
<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="/" 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="/" class="mobile-nav-link">All Tools</a>
</div>
</div>
</nav>
<input type="file" id="pdfUploadInput" accept="application/pdf" class="hidden" />
<div class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
<!-- Upload Area (shown initially) -->
<div id="upload-area"
class="bg-gray-800 rounded-xl shadow-xl p-6 max-w-5xl w-full text-gray-200 border border-gray-700">
<button id="back-to-tools-upload"
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>
<h1 class="text-2xl font-bold text-white mb-2">Create PDF Form</h1>
<p class="text-gray-400 mb-6">
Upload an existing PDF or create a blank PDF to start adding form fields.
</p>
<div class="relative flex flex-col items-center justify-center w-full h-44 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-700 hover:bg-gray-600 transition-colors duration-300"
id="dropZone">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
<p class="mb-2 text-sm text-gray-300">
<span class="font-semibold">Click to select a PDF</span> or drag and drop
</p>
<p class="text-xs text-gray-500">Single PDF file</p>
<p class="text-xs text-gray-500">Your files never leave your device.</p>
</div>
<input type="file" id="pdfFileInput" accept="application/pdf"
class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
</div>
<div id="fileList" class="mt-4 hidden text-sm text-gray-200"></div>
<!-- OR Separator -->
<div class="relative my-6">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-600"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-4 bg-gray-800 text-gray-400 font-medium">OR</span>
</div>
</div>
<div>
<button id="blankPdfBtn"
class="w-full bg-gray-700 hover:bg-gray-600 text-white p-4 rounded-lg flex items-center justify-center gap-3 transition-colors">
<i data-lucide="file-plus" class="w-5 h-5"></i>
<span class="font-semibold">Create Blank PDF</span>
</button>
<div id="pageSizeSelector" class="mt-4 hidden">
<label class="block text-sm font-semibold text-gray-300 mb-2">Page Size:</label>
<select id="pageSizeSelect"
class="w-full bg-gray-600 border border-gray-500 text-white rounded px-3 py-2 text-sm focus:ring-indigo-500 focus:border-indigo-500">
<option value="letter">Letter (8.5" × 11")</option>
<option value="a4">A4 (210mm × 297mm)</option>
<option value="a5">A5 (148mm × 210mm)</option>
<option value="legal">Legal (8.5" × 14")</option>
<option value="tabloid">Tabloid (11" × 17")</option>
<option value="a3">A3 (297mm × 420mm)</option>
<option value="custom">Custom</option>
</select>
<div id="customDimensionsInput" class="mt-3 hidden space-y-2">
<div class="flex gap-2">
<div class="flex-1">
<label class="block text-xs text-gray-400 mb-1">Width (pts)</label>
<input type="number" id="customWidth" placeholder="612" min="72" max="14400"
class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
<div class="flex-1">
<label class="block text-xs text-gray-400 mb-1">Height (pts)</label>
<input type="number" id="customHeight" placeholder="792" min="72" max="14400"
class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
</div>
</div>
<p class="text-xs text-gray-500 italic">1 inch = 72 points</p>
</div>
<button id="confirmBlankBtn"
class="w-full mt-3 bg-indigo-600 hover:bg-indigo-500 text-white p-2 rounded transition-colors">
Start Creating
</button>
</div>
</div>
</div>
<!-- Tool Container (hidden initially) -->
<div id="tool-container"
class="bg-gray-800 rounded-xl shadow-xl p-6 w-full text-gray-200 border border-gray-700 hidden">
<button id="back-to-tools-creator"
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>
<h1 class="text-2xl font-bold text-white mb-2">Create PDF Form</h1>
<p class="text-gray-400 mb-6">
Drag and drop fields onto the canvas to create a fillable PDF form. Customize field properties and
download your form.
</p>
<!-- Main Form Creator Area -->
<div class="flex flex-col lg:flex-row gap-6">
<!-- Sidebar with Properties -->
<aside class="lg:w-64 flex-shrink-0 order-2 lg:order-1">
<!-- Properties Section -->
<div class="bg-gray-700 rounded-lg p-4">
<h2 class="font-semibold text-gray-200 mb-3 flex items-center gap-2">
<i data-lucide="settings" class="w-4 h-4"></i>
Properties
</h2>
<div id="propertiesPanel" class="space-y-3">
<p class="text-gray-500 text-sm">Select a field to edit properties</p>
</div>
</div>
<!-- Field Count -->
<div class="mt-4 bg-gray-700 rounded-lg p-3 text-center">
<p class="text-sm text-gray-400">
<span id="fieldCount" class="text-indigo-400 font-bold text-lg">0</span> field(s)
</p>
<p class="text-xs text-gray-500 mt-1">Press Delete to remove selected field</p>
</div>
</aside>
<!-- Canvas Area -->
<main class="flex-1 order-1 lg:order-2 min-w-0">
<!-- Page Management Toolbar -->
<div class="bg-gray-700 rounded-lg p-3 mb-4 flex flex-wrap gap-2 justify-between items-center">
<div class="flex items-center gap-2">
<button id="prevPageBtn"
class="bg-gray-600 hover:bg-gray-500 text-white p-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled>
<i data-lucide="chevron-left" class="w-4 h-4"></i>
</button>
<span id="pageIndicator" class="text-sm text-gray-300 px-2">Page 1 of 1</span>
<button id="nextPageBtn"
class="bg-gray-600 hover:bg-gray-500 text-white p-2 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled>
<i data-lucide="chevron-right" class="w-4 h-4"></i>
</button>
</div>
<div class="flex items-center gap-2">
<button id="resetBtn"
class="bg-gray-600 hover:bg-gray-500 text-white p-2 px-3 rounded flex items-center gap-2 transition-colors">
<i data-lucide="rotate-ccw" class="w-4 h-4"></i>
<span class="text-sm">Reset</span>
</button>
<button id="addPageBtn"
class="bg-indigo-600 hover:bg-indigo-500 text-white p-2 px-3 rounded flex items-center gap-2 transition-colors">
<i data-lucide="file-plus" class="w-4 h-4"></i>
<span class="text-sm">Add Page</span>
</button>
</div>
</div>
<!-- Top Toolbar -->
<div class="bg-gray-700 rounded-lg p-2 mb-4">
<div class="flex flex-wrap gap-2 justify-center items-center max-w-full overflow-hidden">
<div class="text-gray-400 text-sm font-semibold mr-2 flex items-center gap-1">
<i data-lucide="wrench" class="w-4 h-4"></i> Fields:
</div>
<div class="tool-item bg-gray-600 hover:bg-indigo-600 text-white p-2 rounded cursor-move transition-colors flex items-center gap-2"
draggable="true" data-type="text" title="Text Field">
<i data-lucide="type" class="w-4 h-4"></i>
<span class="text-xs">Text</span>
</div>
<div class="tool-item bg-gray-600 hover:bg-indigo-600 text-white p-2 rounded cursor-move transition-colors flex items-center gap-2"
draggable="true" data-type="checkbox" title="Checkbox">
<i data-lucide="check-square" class="w-4 h-4"></i>
<span class="text-xs">Checkbox</span>
</div>
<div class="tool-item bg-gray-600 hover:bg-indigo-600 text-white p-2 rounded cursor-move transition-colors flex items-center gap-2"
draggable="true" data-type="radio" title="Radio Button">
<i data-lucide="circle-dot" class="w-4 h-4"></i>
<span class="text-xs">Radio</span>
</div>
<div class="tool-item bg-gray-600 hover:bg-indigo-600 text-white p-2 rounded cursor-move transition-colors flex items-center gap-2"
draggable="true" data-type="dropdown" title="Dropdown">
<i data-lucide="chevron-down-square" class="w-4 h-4"></i>
<span class="text-xs">Dropdown</span>
</div>
<div class="tool-item bg-gray-600 hover:bg-indigo-600 text-white p-2 rounded cursor-move transition-colors flex items-center gap-2"
draggable="true" data-type="optionlist" title="Option List">
<i data-lucide="list" class="w-4 h-4"></i>
<span class="text-xs">List</span>
</div>
<div class="tool-item bg-gray-600 hover:bg-indigo-600 text-white p-2 rounded cursor-move transition-colors flex items-center gap-2"
draggable="true" data-type="button" title="Button">
<i data-lucide="square-mouse-pointer" class="w-4 h-4"></i>
<span class="text-xs">Button</span>
</div>
<div class="tool-item bg-gray-600 hover:bg-indigo-600 text-white p-2 rounded cursor-move transition-colors flex items-center gap-2"
draggable="true" data-type="signature" title="Signature">
<i data-lucide="pen-tool" class="w-4 h-4"></i>
<span class="text-xs">Signature</span>
</div>
<div class="tool-item bg-gray-600 hover:bg-indigo-600 text-white p-2 rounded cursor-move transition-colors flex items-center gap-2"
draggable="true" data-type="date" title="Date Field">
<i data-lucide="calendar" class="w-4 h-4"></i>
<span class="text-xs">Date</span>
</div>
<div class="tool-item bg-gray-600 hover:bg-indigo-600 text-white p-2 rounded cursor-move transition-colors flex items-center gap-2"
draggable="true" data-type="image" title="Image Field">
<i data-lucide="image" class="w-4 h-4"></i>
<span class="text-xs">Image</span>
</div>
</div>
</div>
<div class="bg-gray-700 rounded-lg p-4 mb-4 overflow-auto w-full">
<div class="flex justify-center">
<div id="pdfCanvasWrapper" class="shadow-2xl relative" style="border: 1px solid #374151;">
<div id="pdfCanvas" class="bg-white relative">
<!-- Fields will be added here dynamically -->
</div>
</div>
</div>
</div>
<!-- Download Button -->
<button id="downloadBtn" class="btn-gradient w-full flex items-center justify-center gap-2 py-3">
<span>Download PDF Form</span>
</button>
</main>
</div>
</div>
</div>
<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="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" />
<span class="text-xl font-bold text-white">BentoPDF</span>
</div>
<p class="text-gray-400 text-sm">
&copy; 2025 BentoPDF. All rights reserved.
</p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span>
</p>
</div>
<div>
<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>
</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>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li>
<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>
</li>
</ul>
</div>
<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"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/AP2Y97juZT" 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" />
</svg>
</a>
<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">
<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">
<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" />
</svg>
</a>
</div>
</div>
</div>
</div>
</footer>
<!-- Custom Modal -->
<div id="errorModal" class="hidden fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-75">
<div class="bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 border border-gray-700">
<div class="p-6">
<h3 id="errorModalTitle" class="text-xl font-bold text-white mb-3">Error</h3>
<p id="errorModalMessage" class="text-gray-300 mb-6">An error occurred.</p>
<button id="errorModalClose"
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded transition-colors">
Close
</button>
</div>
</div>
</div>
</div>
<script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../version.ts"></script>
<script type="module" src="../js/logic/form-creator.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script>
</body>
</html>
+4 -11
View File
@@ -59,8 +59,7 @@
</nav> </nav>
<div class="min-h-screen flex items-center justify-center p-4 bg-gray-900"> <div class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
<div id="tool-uploader" <div class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
<button id="back-to-tools" <button id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"> class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i> <i data-lucide="arrow-left" class="cursor-pointer"></i>
@@ -105,7 +104,7 @@
</div> </div>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8"> <footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div 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="mb-8 md:mb-0">
@@ -117,7 +116,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -139,9 +138,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -162,7 +158,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -190,9 +186,6 @@
</footer> </footer>
<script type="module" src="../js/logic/json-to-pdf.ts"></script> <script type="module" src="../js/logic/json-to-pdf.ts"></script>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../version.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
+6 -6
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en" class="overflow-hidden"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -36,10 +36,10 @@
</style> </style>
</head> </head>
<body class="antialiased bg-gray-900 h-[100dvh] overflow-hidden flex flex-col"> <body class="antialiased bg-gray-900 h-screen overflow-hidden">
<!-- Navigation --> <!-- Navigation -->
<nav class="bg-gray-800 border-b border-gray-700 flex-none z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center"> <div class="flex-shrink-0 flex items-center">
@@ -58,10 +58,10 @@
</nav> </nav>
<!-- Main Container --> <!-- Main Container -->
<div class="flex flex-col flex-1 overflow-hidden relative"> <div class="flex flex-col h-[calc(100vh-4rem)]">
<!-- Toolbar --> <!-- Toolbar -->
<div class="bg-gray-800 border-b border-gray-700 p-2 sm:p-3 toolbar-container flex-none z-20"> <div class="bg-gray-800 border-b border-gray-700 p-2 sm:p-3 overflow-x-auto scrollbar-hide toolbar-container sm:sticky sm:top-16 sm:z-20">
<div <div
class="flex flex-wrap items-center justify-center sm:justify-start gap-2 bg-gray-900 p-2 sm:p-4 rounded-lg w-full"> class="flex flex-wrap items-center justify-center sm:justify-start gap-2 bg-gray-900 p-2 sm:p-4 rounded-lg w-full">
@@ -186,7 +186,7 @@
</div> </div>
<!-- Content Area --> <!-- Content Area -->
<div id="main-scroll-container" class="flex-1 overflow-auto p-3 pb-24 sm:p-4 sm:pb-8 md:pt-4 md:pb-4"> <div class="flex-1 overflow-auto p-3 sm:p-4 sm:pt-20 sm:pb-8 md:pt-4 md:pb-4">
<div id="upload-area" <div id="upload-area"
class="hidden border-2 border-dashed border-gray-600 rounded-lg p-6 sm:p-12 text-center max-w-full cursor-pointer" class="hidden border-2 border-dashed border-gray-600 rounded-lg p-6 sm:p-12 text-center max-w-full cursor-pointer"
onclick="document.getElementById('pdf-file-input').click()"> onclick="document.getElementById('pdf-file-input').click()">
+4 -11
View File
@@ -59,8 +59,7 @@
</nav> </nav>
<div class="min-h-screen flex items-center justify-center p-4 bg-gray-900"> <div class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
<div id="tool-uploader" <div class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
<button id="back-to-tools" <button id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"> class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i> <i data-lucide="arrow-left" class="cursor-pointer"></i>
@@ -99,7 +98,7 @@
</div> </div>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8"> <footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div 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="mb-8 md:mb-0">
@@ -111,7 +110,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -133,9 +132,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -156,7 +152,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -184,9 +180,6 @@
</footer> </footer>
<script type="module" src="../js/logic/pdf-to-json.ts"></script> <script type="module" src="../js/logic/pdf-to-json.ts"></script>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../version.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
+3 -12
View File
@@ -61,8 +61,7 @@
</nav> </nav>
<div id="uploader" class="min-h-screen flex items-center justify-center p-4 bg-gray-900"> <div id="uploader" class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
<div id="tool-uploader" <div class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
<button id="back-to-tools" <button id="back-to-tools"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold"> class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i> <i data-lucide="arrow-left" class="cursor-pointer"></i>
@@ -171,7 +170,7 @@
</div> </div>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8"> <footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div 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="mb-8 md:mb-0">
@@ -182,9 +181,6 @@
<p class="text-gray-400 text-sm"> <p class="text-gray-400 text-sm">
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span>
</p>
</div> </div>
<div> <div>
@@ -205,9 +201,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -228,7 +221,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -255,8 +248,6 @@
</div> </div>
</footer> </footer>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../js/logic/table-of-contents.ts"></script> <script type="module" src="../js/logic/table-of-contents.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
-120
View File
@@ -1,120 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { txtToPdf, setupTxtToPdfTool } from '@/js/logic/txt-to-pdf';
import * as ui from '@/js/ui';
import * as helpers from '@/js/utils/helpers';
import { state } from '@/js/state';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
// Mocks
vi.mock('@/js/ui', () => ({
showLoader: vi.fn(),
hideLoader: vi.fn(),
showAlert: vi.fn(),
}));
vi.mock('@/js/utils/helpers', () => ({
downloadFile: vi.fn(),
hexToRgb: vi.fn(() => ({ r: 0, g: 0, b: 0 })),
}));
vi.mock('pdf-lib', () => ({
PDFDocument: {
create: vi.fn(),
},
StandardFonts: {
Helvetica: 'Helvetica',
},
PageSizes: {
A4: [595.28, 841.89],
},
rgb: vi.fn(),
}));
vi.mock('jszip', () => {
return {
default: vi.fn().mockImplementation(() => ({
file: vi.fn(),
generateAsync: vi.fn().mockResolvedValue(new Blob()),
})),
};
});
describe('Text to PDF Tool', () => {
let mockPdfDoc: any;
let mockPage: any;
let mockFont: any;
beforeEach(() => {
document.body.innerHTML = `
<button id="txt-mode-upload-btn"></button>
<button id="txt-mode-text-btn"></button>
<div id="txt-upload-panel"></div>
<div id="txt-text-panel" class="hidden"></div>
<button id="process-btn"></button>
<select id="font-family"><option value="Helvetica">Helvetica</option></select>
<input id="font-size" value="12" />
<select id="page-size"><option value="A4">A4</option></select>
<input id="text-color" value="#000000" />
<textarea id="text-input"></textarea>
`;
mockPage = {
getSize: vi.fn(() => ({ width: 595.28, height: 841.89 })),
drawText: vi.fn(),
getHeight: vi.fn(() => 841.89),
};
mockFont = {
widthOfTextAtSize: vi.fn(() => 10),
};
mockPdfDoc = {
embedFont: vi.fn().mockResolvedValue(mockFont),
addPage: vi.fn(() => mockPage),
save: vi.fn().mockResolvedValue(new Uint8Array([])),
};
vi.mocked(PDFLibDocument.create).mockResolvedValue(mockPdfDoc);
state.files = [];
});
afterEach(() => {
vi.clearAllMocks();
});
it('setupTxtToPdfTool should attach onclick listener to process-btn', async () => {
const processBtn = document.getElementById('process-btn');
expect(processBtn?.onclick).toBeNull();
await setupTxtToPdfTool();
expect(processBtn?.onclick).toBeDefined();
});
it('txtToPdf should handle text input mode', async () => {
// Switch to text mode (simulate UI state)
document.getElementById('txt-upload-panel')?.classList.add('hidden');
document.getElementById('txt-text-panel')?.classList.remove('hidden');
(document.getElementById('text-input') as HTMLTextAreaElement).value = 'Hello World';
await txtToPdf();
expect(ui.showLoader).toHaveBeenCalled();
expect(PDFLibDocument.create).toHaveBeenCalled();
expect(mockPdfDoc.addPage).toHaveBeenCalled();
expect(mockPage.drawText).toHaveBeenCalled();
expect(helpers.downloadFile).toHaveBeenCalled();
expect(ui.hideLoader).toHaveBeenCalled();
});
it('txtToPdf should show alert if text input is empty', async () => {
document.getElementById('txt-upload-panel')?.classList.add('hidden');
document.getElementById('txt-text-panel')?.classList.remove('hidden');
(document.getElementById('text-input') as HTMLTextAreaElement).value = ' ';
await txtToPdf();
expect(ui.showAlert).toHaveBeenCalledWith('Input Required', 'Please enter some text to convert.');
expect(PDFLibDocument.create).not.toHaveBeenCalled();
});
});
-18
View File
@@ -1,18 +0,0 @@
import packageJson from '../package.json';
export const APP_VERSION = packageJson.version;
export function injectVersion() {
const versionElements = document.querySelectorAll('#app-version, #app-version-simple');
versionElements.forEach((element) => {
element.textContent = APP_VERSION;
});
}
if (typeof document !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectVersion);
} else {
injectVersion();
}
}
+59 -25
View File
@@ -1,6 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -8,13 +7,19 @@
<link rel="icon" type="image/png" href="images/favicon.svg" /> <link rel="icon" type="image/png" href="images/favicon.svg" />
<link href="/src/css/styles.css" rel="stylesheet" /> <link href="/src/css/styles.css" rel="stylesheet" />
</head> </head>
<body class="antialiased bg-gray-900 text-gray-300"> <body class="antialiased bg-gray-900 text-gray-300">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> 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"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
@@ -25,25 +30,52 @@
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a> <a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center"> <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" 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> <span class="sr-only">Open main menu</span>
<!-- Hamburger Icon --> <!-- 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" <svg
stroke="currentColor" aria-hidden="true"> id="menu-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> 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> </svg>
<!-- Close Icon --> <!-- Close Icon -->
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" <svg
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> id="close-icon"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> 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> </svg>
</button> </button>
</div> </div>
@@ -51,13 +83,17 @@
</div> </div>
<!-- Mobile Menu Dropdown --> <!-- Mobile Menu Dropdown -->
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700"> <div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center"> <div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a> <a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="./licensing.html" class="mobile-nav-link">Licensing</a> <a href="index.html#tools-header" class="mobile-nav-link"
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a> >All Tools</a
>
</div> </div>
</div> </div>
</nav> </nav>
@@ -85,8 +121,10 @@
manipulating Portable Document Format (PDF) files. All operations manipulating Portable Document Format (PDF) files. All operations
performed by the Service are executed locally within your web performed by the Service are executed locally within your web
browser. browser.
<strong>No files or data are uploaded to or stored on our <strong
servers.</strong> >No files or data are uploaded to or stored on our
servers.</strong
>
</p> </p>
<h2>3. User Conduct and Responsibilities</h2> <h2>3. User Conduct and Responsibilities</h2>
@@ -196,7 +234,7 @@
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2"> <p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span> Version <span id="app-version">1.6.2</span>
</p> </p>
</div> </div>
@@ -218,9 +256,6 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <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>
@@ -241,7 +276,7 @@
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
</a> </a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer" <a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord"> 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"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
@@ -268,8 +303,7 @@
</div> </div>
</footer> </footer>
<script type="module" src="src/js/utils/lucide-init.ts"></script> <script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
-1
View File
@@ -54,7 +54,6 @@
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
/* Path aliases (optional but helpful) */ /* Path aliases (optional but helpful) */
"baseUrl": ".", "baseUrl": ".",
-10
View File
@@ -29,13 +29,6 @@ export default defineConfig(({ mode }) => ({
exclude: ['coherentpdf'], exclude: ['coherentpdf'],
}, },
server: { server: {
host: true,
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
preview: {
headers: { headers: {
'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp', 'Cross-Origin-Embedder-Policy': 'require-corp',
@@ -51,7 +44,6 @@ export default defineConfig(({ mode }) => ({
privacy: resolve(__dirname, 'privacy.html'), privacy: resolve(__dirname, 'privacy.html'),
terms: resolve(__dirname, 'terms.html'), terms: resolve(__dirname, 'terms.html'),
bookmark: resolve(__dirname, 'src/pages/bookmark.html'), bookmark: resolve(__dirname, 'src/pages/bookmark.html'),
licensing: resolve(__dirname, 'licensing.html'),
'table-of-contents': resolve( 'table-of-contents': resolve(
__dirname, __dirname,
'src/pages/table-of-contents.html' 'src/pages/table-of-contents.html'
@@ -59,8 +51,6 @@ export default defineConfig(({ mode }) => ({
'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'), 'pdf-to-json': resolve(__dirname, 'src/pages/pdf-to-json.html'),
'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'), 'json-to-pdf': resolve(__dirname, 'src/pages/json-to-pdf.html'),
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'), 'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
'add-stamps': resolve(__dirname, 'src/pages/add-stamps.html'),
'form-creator': resolve(__dirname, 'src/pages/form-creator.html'),
}, },
}, },
}, },