mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-05 11:52:48 +01:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3f6af079f | ||
|
|
6e7563d0c2 | ||
|
|
b9572dfea8 | ||
|
|
e8c0e82d29 | ||
|
|
95927cd899 | ||
|
|
1e8866018d | ||
|
|
83aadb6d98 | ||
|
|
1a17cd4e7e | ||
|
|
5fecc701c6 | ||
|
|
77ee986e2c | ||
|
|
606a3603f4 | ||
|
|
8a8ae3f0c5 | ||
|
|
092f84be23 | ||
|
|
2aa26e496c | ||
|
|
900196879f | ||
|
|
bd985f06f8 | ||
|
|
98467f4809 | ||
|
|
151060204d | ||
|
|
3019195fe5 | ||
|
|
3673b6e434 | ||
|
|
ad915b7527 | ||
|
|
bbafb11246 | ||
|
|
e33e79ef4d | ||
|
|
1e557d5b2a |
@@ -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@v6
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: "20"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<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.
|
||||||
|
|
||||||
 [](https://ko-fi.com/alio0) 
|
 [](https://ko-fi.com/alio0) 
|
||||||
@@ -10,6 +8,17 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 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
|
||||||
@@ -62,6 +71,7 @@ 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. |
|
||||||
@@ -180,7 +190,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.6.2 # Replace with your version
|
cd dist-1.7.3 # Replace with your version
|
||||||
```
|
```
|
||||||
|
|
||||||
**Serve with Python:**
|
**Serve with Python:**
|
||||||
@@ -435,7 +445,3 @@ 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.
|
|
||||||
|
|||||||
+258
-319
@@ -1,337 +1,276 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>About Bentopdf - Fast, Private, and Free PDF Tools</title>
|
|
||||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<head>
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<meta charset="UTF-8" />
|
||||||
<div class="container mx-auto px-4">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<div class="flex justify-between items-center h-16">
|
<title>About Bentopdf - Fast, Private, and Free PDF Tools</title>
|
||||||
<div
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
</head>
|
||||||
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 -->
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="container mx-auto px-4">
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<div class="flex justify-between items-center h-16">
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||||
</div>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<button
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
id="mobile-menu-button"
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
type="button"
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
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"
|
<a href="./licensing.html" class="nav-link">Licensing</a>
|
||||||
aria-controls="mobile-menu"
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
aria-expanded="false"
|
</div>
|
||||||
>
|
|
||||||
<span class="sr-only">Open main menu</span>
|
<!-- Mobile Hamburger Button -->
|
||||||
<!-- Hamburger Icon -->
|
<div class="md:hidden flex items-center">
|
||||||
<svg
|
<button id="mobile-menu-button" type="button"
|
||||||
id="menu-icon"
|
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="block h-6 w-6"
|
aria-controls="mobile-menu" aria-expanded="false">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<span class="sr-only">Open main menu</span>
|
||||||
fill="none"
|
<!-- Hamburger Icon -->
|
||||||
viewBox="0 0 24 24"
|
<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"
|
stroke="currentColor" aria-hidden="true">
|
||||||
aria-hidden="true"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
>
|
</svg>
|
||||||
<path
|
<!-- Close Icon -->
|
||||||
stroke-linecap="round"
|
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
stroke-linejoin="round"
|
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
</svg>
|
||||||
/>
|
</button>
|
||||||
</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>
|
||||||
</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="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="about-hero" class="text-center py-16 md:py-24">
|
|
||||||
<h1 class="text-3xl md:text-6xl font-bold text-white mb-4">
|
|
||||||
We believe PDF tools should be
|
|
||||||
<span class="marker-slanted">fast, private, and free.</span>
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg md:text-xl text-gray-400">No compromises.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
|
||||||
|
|
||||||
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
|
|
||||||
<div class="text-center">
|
|
||||||
<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">
|
|
||||||
Our Mission
|
|
||||||
</h2>
|
|
||||||
<p class="text-lg text-gray-400 leading-relaxed">
|
|
||||||
To provide the most comprehensive PDF toolbox that respects your
|
|
||||||
privacy and never asks for payment. We believe essential document
|
|
||||||
tools should be accessible to everyone, everywhere, without
|
|
||||||
barriers.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<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="text-center md:text-left">
|
|
||||||
<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">
|
|
||||||
Privacy First. Always.
|
|
||||||
</h2>
|
|
||||||
<p class="text-gray-400 leading-relaxed">
|
|
||||||
In an era where data is a commodity, we take a different approach.
|
|
||||||
All processing for Bentopdf tools happens locally in your browser.
|
|
||||||
This means your files never touch our servers, we never see your
|
|
||||||
documents, and we don't track what you do. Your documents remain
|
|
||||||
completely and unequivocally private. It's not just a feature;
|
|
||||||
it's our foundation.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div class="relative w-48 h-48">
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"
|
|
||||||
></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>
|
|
||||||
|
|
||||||
<section id="why-Bentopdf" class="py-16">
|
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
|
|
||||||
Why <span class="marker-slanted">Bentopdf?</span>
|
|
||||||
</h2>
|
|
||||||
<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">
|
|
||||||
<i
|
|
||||||
data-lucide="zap"
|
|
||||||
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
|
||||||
></i>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white">Built for Speed</h3>
|
|
||||||
<p class="text-gray-400 mt-2">
|
|
||||||
No waiting for uploads or downloads to a server. By processing
|
|
||||||
files directly in your browser using modern web technologies
|
|
||||||
like WebAssembly, we offer unparalleled speed for all our tools.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white">Completely Free</h3>
|
|
||||||
<p class="text-gray-400 mt-2">
|
|
||||||
No trials, no subscriptions, no hidden fees, and no "premium"
|
|
||||||
features held hostage. We believe powerful PDF tools should be a
|
|
||||||
public utility, not a profit center.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white">No Account Required</h3>
|
|
||||||
<p class="text-gray-400 mt-2">
|
|
||||||
Start using any tool immediately. We don't need your email, a
|
|
||||||
password, or any personal information. Your workflow should be
|
|
||||||
frictionless and anonymous.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white">Open Source Spirit</h3>
|
|
||||||
<p class="text-gray-400 mt-2">
|
|
||||||
Built with transparency in mind. We leverage incredible
|
|
||||||
open-source libraries like PDF-lib and PDF.js, and believe in
|
|
||||||
the community-driven effort to make powerful tools accessible to
|
|
||||||
everyone.
|
|
||||||
</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">
|
|
||||||
Ready to get started?
|
|
||||||
</h2>
|
|
||||||
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
|
||||||
Join thousands of users who trust Bentopdf for their daily document
|
|
||||||
needs. Experience the difference that privacy and performance can
|
|
||||||
make.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
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
|
|
||||||
</a>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div class="container mx-auto px-4">
|
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<div class="mb-8 md:mb-0">
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<a href="./licensing.html" class="mobile-nav-link">Licensing</a>
|
||||||
</div>
|
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||||
<p class="text-gray-400 text-sm">
|
</div>
|
||||||
© 2025 BentoPDF. All rights reserved.
|
</div>
|
||||||
</p>
|
</nav>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
|
||||||
Version <span id="app-version">1.6.2</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<section id="about-hero" class="text-center py-16 md:py-24">
|
||||||
<ul class="space-y-2 text-gray-400">
|
<h1 class="text-3xl md:text-6xl font-bold text-white mb-4">
|
||||||
<li>
|
We believe PDF tools should be
|
||||||
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
<span class="marker-slanted">fast, private, and free.</span>
|
||||||
</li>
|
</h1>
|
||||||
<li>
|
<p class="text-lg md:text-xl text-gray-400">No compromises.</p>
|
||||||
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
</section>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="section-divider"></div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
|
||||||
<ul class="space-y-2 text-gray-400">
|
|
||||||
<li>
|
|
||||||
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<div class="text-center">
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<i data-lucide="rocket" class="w-16 h-16 text-indigo-400 mx-auto mb-6"></i>
|
||||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
Our Mission
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
</h2>
|
||||||
<path fill-rule="evenodd"
|
<p class="text-lg text-gray-400 leading-relaxed">
|
||||||
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"
|
To provide the most comprehensive PDF toolbox that respects your
|
||||||
clip-rule="evenodd" />
|
privacy and never asks for payment. We believe essential document
|
||||||
</svg>
|
tools should be accessible to everyone, everywhere, without
|
||||||
</a>
|
barriers.
|
||||||
<a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
</p>
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
</div>
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
</section>
|
||||||
<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" />
|
<div class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700">
|
||||||
</svg>
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||||
</a>
|
<div class="text-center md:text-left">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
<span class="text-indigo-400 font-bold uppercase">Our Core Philosophy</span>
|
||||||
title="Instagram">
|
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">
|
||||||
<i data-lucide="instagram"></i>
|
Privacy First. Always.
|
||||||
</a>
|
</h2>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
<p class="text-gray-400 leading-relaxed">
|
||||||
title="LinkedIn">
|
In an era where data is a commodity, we take a different approach.
|
||||||
<i data-lucide="linkedin"></i>
|
All processing for Bentopdf tools happens locally in your browser.
|
||||||
</a>
|
This means your files never touch our servers, we never see your
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
|
documents, and we don't track what you do. Your documents remain
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
completely and unequivocally private. It's not just a feature;
|
||||||
<path
|
it's our foundation.
|
||||||
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" />
|
</p>
|
||||||
</svg>
|
</div>
|
||||||
</a>
|
<div class="flex justify-center">
|
||||||
</div>
|
<div class="relative w-48 h-48">
|
||||||
|
<div class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"></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>
|
||||||
</footer>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
<section id="why-Bentopdf" class="py-16">
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
|
||||||
|
Why <span class="marker-slanted">Bentopdf?</span>
|
||||||
|
</h2>
|
||||||
|
<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">
|
||||||
|
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white">Built for Speed</h3>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
No waiting for uploads or downloads to a server. By processing
|
||||||
|
files directly in your browser using modern web technologies
|
||||||
|
like WebAssembly, we offer unparalleled speed for all our tools.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white">Completely Free</h3>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
No trials, no subscriptions, no hidden fees, and no "premium"
|
||||||
|
features held hostage. We believe powerful PDF tools should be a
|
||||||
|
public utility, not a profit center.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white">No Account Required</h3>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
Start using any tool immediately. We don't need your email, a
|
||||||
|
password, or any personal information. Your workflow should be
|
||||||
|
frictionless and anonymous.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white">Open Source Spirit</h3>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
Built with transparency in mind. We leverage incredible
|
||||||
|
open-source libraries like PDF-lib and PDF.js, and believe in
|
||||||
|
the community-driven effort to make powerful tools accessible to
|
||||||
|
everyone.
|
||||||
|
</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">
|
||||||
|
Ready to get started?
|
||||||
|
</h2>
|
||||||
|
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||||
|
Join thousands of users who trust Bentopdf for their daily document
|
||||||
|
needs. Experience the difference that privacy and performance can
|
||||||
|
make.
|
||||||
|
</p>
|
||||||
|
<a 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
|
||||||
|
</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">
|
||||||
|
© 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/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
+153
-189
@@ -1,209 +1,173 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Contact Us - BentoPDF</title>
|
|
||||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<head>
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<meta charset="UTF-8" />
|
||||||
<div class="container mx-auto px-4">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<div class="flex justify-between items-center h-16">
|
<title>Contact Us - BentoPDF</title>
|
||||||
<div
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
</head>
|
||||||
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 -->
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="container mx-auto px-4">
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<div class="flex justify-between items-center h-16">
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||||
</div>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
<!-- Mobile Hamburger Button -->
|
</span>
|
||||||
<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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Mobile Menu Dropdown -->
|
<!-- Desktop Navigation -->
|
||||||
<div
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
id="mobile-menu"
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
<a href="./licensing.html" class="nav-link">Licensing</a>
|
||||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
|
||||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
|
||||||
<a href="index.html#tools-header" class="mobile-nav-link"
|
|
||||||
>All Tools</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<!-- Mobile Hamburger Button -->
|
||||||
<section id="contact-hero" class="text-center py-16 md:py-24">
|
<div class="md:hidden flex items-center">
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
<button id="mobile-menu-button" type="button"
|
||||||
Get in Touch
|
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"
|
||||||
</h1>
|
aria-controls="mobile-menu" aria-expanded="false">
|
||||||
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
<span class="sr-only">Open main menu</span>
|
||||||
We'd love to hear from you. Whether you have a question, feedback, or
|
<!-- Hamburger Icon -->
|
||||||
a feature request, please don't hesitate to reach out.
|
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
</p>
|
stroke="currentColor" aria-hidden="true">
|
||||||
</section>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
<div class="max-w-2xl mx-auto text-center py-8">
|
<!-- Close Icon -->
|
||||||
<p class="text-lg text-gray-400">
|
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
You can reach us directly by email at:
|
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
<a
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
href="mailto:[email protected]"
|
</svg>
|
||||||
class="text-indigo-400 underline hover:text-indigo-300"
|
</button>
|
||||||
>[email protected]</a
|
</div>
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div class="container mx-auto px-4">
|
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<div class="mb-8 md:mb-0">
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<a href="./licensing.html" class="mobile-nav-link">Licensing</a>
|
||||||
</div>
|
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||||
<p class="text-gray-400 text-sm">
|
</div>
|
||||||
© 2025 BentoPDF. All rights reserved.
|
</div>
|
||||||
</p>
|
</nav>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
|
||||||
Version <span id="app-version">1.6.2</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<section id="contact-hero" class="text-center py-16 md:py-24">
|
||||||
<ul class="space-y-2 text-gray-400">
|
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||||
<li>
|
Get in Touch
|
||||||
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
</h1>
|
||||||
</li>
|
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
||||||
<li>
|
We'd love to hear from you. Whether you have a question, feedback, or
|
||||||
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
a feature request, please don't hesitate to reach out.
|
||||||
</li>
|
</p>
|
||||||
<li>
|
</section>
|
||||||
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="max-w-2xl mx-auto text-center py-8">
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<p class="text-lg text-gray-400">
|
||||||
<ul class="space-y-2 text-gray-400">
|
You can reach us directly by email at:
|
||||||
<li>
|
<a href="mailto:[email protected]"
|
||||||
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
class="text-indigo-400 underline hover:text-indigo-300">[email protected]</a>
|
||||||
</li>
|
</p>
|
||||||
<li>
|
</div>
|
||||||
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
</div>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
<div class="mb-8 md:mb-0">
|
||||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||||
<path fill-rule="evenodd"
|
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||||
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"
|
</div>
|
||||||
clip-rule="evenodd" />
|
<p class="text-gray-400 text-sm">
|
||||||
</svg>
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</a>
|
</p>
|
||||||
<a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
Version <span id="app-version"></span>
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
</p>
|
||||||
<path
|
</div>
|
||||||
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>
|
<div>
|
||||||
</a>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
<ul class="space-y-2 text-gray-400">
|
||||||
title="Instagram">
|
<li>
|
||||||
<i data-lucide="instagram"></i>
|
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
</a>
|
</li>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
<li>
|
||||||
title="LinkedIn">
|
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||||
<i data-lucide="linkedin"></i>
|
</li>
|
||||||
</a>
|
<li>
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
|
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
</li>
|
||||||
<path
|
</ul>
|
||||||
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" />
|
</div>
|
||||||
</svg>
|
|
||||||
</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
|
</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>
|
||||||
|
</body>
|
||||||
|
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,395 +1,320 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Frequently Asked Questions - BentoPDF</title>
|
|
||||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<head>
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<meta charset="UTF-8" />
|
||||||
<div class="container mx-auto px-4">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<div class="flex justify-between items-center h-16">
|
<title>Frequently Asked Questions - BentoPDF</title>
|
||||||
<div
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
</head>
|
||||||
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 -->
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="container mx-auto px-4">
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<div class="flex justify-between items-center h-16">
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||||
</div>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<button
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
id="mobile-menu-button"
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
type="button"
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
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"
|
<a href="./licensing.html" class="nav-link">Licensing</a>
|
||||||
aria-controls="mobile-menu"
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
aria-expanded="false"
|
</div>
|
||||||
>
|
|
||||||
<span class="sr-only">Open main menu</span>
|
<!-- Mobile Hamburger Button -->
|
||||||
<!-- Hamburger Icon -->
|
<div class="md:hidden flex items-center">
|
||||||
<svg
|
<button id="mobile-menu-button" type="button"
|
||||||
id="menu-icon"
|
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="block h-6 w-6"
|
aria-controls="mobile-menu" aria-expanded="false">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<span class="sr-only">Open main menu</span>
|
||||||
fill="none"
|
<!-- Hamburger Icon -->
|
||||||
viewBox="0 0 24 24"
|
<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"
|
stroke="currentColor" aria-hidden="true">
|
||||||
aria-hidden="true"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
>
|
</svg>
|
||||||
<path
|
<!-- Close Icon -->
|
||||||
stroke-linecap="round"
|
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
stroke-linejoin="round"
|
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
</svg>
|
||||||
/>
|
</button>
|
||||||
</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>
|
||||||
</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="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="faq-hero" class="text-center py-16 md:py-24">
|
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
|
||||||
Frequently Asked Questions
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
|
||||||
Have questions? We've got answers. Here are some of the most common
|
|
||||||
things people ask about BentoPDF.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto space-y-4">
|
|
||||||
<details
|
|
||||||
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
|
||||||
>
|
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
|
||||||
<h3 class="font-semibold text-white text-lg">
|
|
||||||
Are my files safe and private?
|
|
||||||
</h3>
|
|
||||||
<i
|
|
||||||
data-lucide="plus"
|
|
||||||
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>
|
|
||||||
<strong>Absolutely.</strong> This is the most important feature of
|
|
||||||
BentoPDF. All processing happens 100% locally in your web browser.
|
|
||||||
Your files are never uploaded to any server, which means we—or
|
|
||||||
anyone else—can never see them. Your privacy is guaranteed.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details
|
|
||||||
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
|
||||||
>
|
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
|
||||||
<h3 class="font-semibold text-white text-lg">
|
|
||||||
Is BentoPDF really free? What's the catch?
|
|
||||||
</h3>
|
|
||||||
<i
|
|
||||||
data-lucide="plus"
|
|
||||||
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>
|
|
||||||
Yes, it's completely free, and there's no catch. There are no
|
|
||||||
hidden fees, no subscription plans, no usage limits, and no
|
|
||||||
premium-only features. We believe essential tools should be
|
|
||||||
accessible to everyone.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details
|
|
||||||
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
|
||||||
>
|
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
|
||||||
<h3 class="font-semibold text-white text-lg">
|
|
||||||
Do I need an internet connection to use the tools?
|
|
||||||
</h3>
|
|
||||||
<i
|
|
||||||
data-lucide="plus"
|
|
||||||
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>
|
|
||||||
After you load the website for the first time, most tools will
|
|
||||||
work completely offline! Because all the processing libraries are
|
|
||||||
loaded into your browser, you can disconnect from the internet and
|
|
||||||
continue to merge, split, compress, and edit your PDFs securely.
|
|
||||||
Some niche tools that require external data (like Markdown to PDF
|
|
||||||
with web images) may need a connection.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details
|
|
||||||
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
|
||||||
>
|
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
|
||||||
<h3 class="font-semibold text-white text-lg">
|
|
||||||
Are there any file size or usage limitations?
|
|
||||||
</h3>
|
|
||||||
<i
|
|
||||||
data-lucide="plus"
|
|
||||||
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>
|
|
||||||
No, we do not impose any artificial limits on file size, the
|
|
||||||
number of files, or how many times you can use a tool. The only
|
|
||||||
practical limitation is the processing power and memory of your
|
|
||||||
own computer, as very large or complex files may take longer to
|
|
||||||
process.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details
|
|
||||||
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
|
||||||
>
|
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
|
||||||
<h3 class="font-semibold text-white text-lg">
|
|
||||||
Why did my PDF fail to process?
|
|
||||||
</h3>
|
|
||||||
<i
|
|
||||||
data-lucide="plus"
|
|
||||||
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>Failures are rare but can happen for a few reasons:</p>
|
|
||||||
<ul class="list-disc list-inside mt-2 space-y-1">
|
|
||||||
<li>
|
|
||||||
The PDF might be corrupted or not compliant with standard
|
|
||||||
specifications.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
The file could be encrypted with a password you don't have.
|
|
||||||
Please use our Decrypt tool first.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
The file might be a special "XFA" or dynamic form-based PDF,
|
|
||||||
which some of our tools cannot process.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details
|
|
||||||
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
|
||||||
>
|
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
|
||||||
<h3 class="font-semibold text-white text-lg">
|
|
||||||
Do you track my activity on BentoPDF?
|
|
||||||
</h3>
|
|
||||||
<i
|
|
||||||
data-lucide="plus"
|
|
||||||
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>
|
|
||||||
We care about your privacy. BentoPDF does not track personal
|
|
||||||
information. We use
|
|
||||||
<a
|
|
||||||
href="https://simpleanalytics.com"
|
|
||||||
class="text-indigo-400 hover:underline"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>Simple Analytics</a
|
|
||||||
>
|
|
||||||
solely to see anonymous visit counts. This means we can know how
|
|
||||||
many users visit our site, but
|
|
||||||
<strong>we never know who you are</strong>. Simple Analytics is
|
|
||||||
fully GDPR-compliant and respects your privacy.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details
|
|
||||||
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
|
||||||
>
|
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
|
||||||
<h3 class="font-semibold text-white text-lg">
|
|
||||||
What technology does BentoPDF use?
|
|
||||||
</h3>
|
|
||||||
<i
|
|
||||||
data-lucide="plus"
|
|
||||||
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
|
||||||
></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>
|
|
||||||
BentoPDF is built on the power of modern web technologies. We
|
|
||||||
primarily use JavaScript libraries like
|
|
||||||
<strong>PDF-lib.js</strong> and <strong>PDFKit.js</strong> that
|
|
||||||
are compiled to run efficiently in your browser via WebAssembly.
|
|
||||||
This allows for powerful, server-like processing without your data
|
|
||||||
ever leaving your machine.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-divider mt-16"></div>
|
|
||||||
|
|
||||||
<section id="contact-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">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
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
|
|
||||||
</a>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div class="container mx-auto px-4">
|
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<div class="mb-8 md:mb-0">
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<a href="./licensing.html" class="mobile-nav-link">Licensing</a>
|
||||||
</div>
|
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||||
<p class="text-gray-400 text-sm">
|
</div>
|
||||||
© 2025 BentoPDF. All rights reserved.
|
</div>
|
||||||
</p>
|
</nav>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
|
||||||
Version <span id="app-version">1.6.2</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<section id="faq-hero" class="text-center py-16 md:py-24">
|
||||||
<ul class="space-y-2 text-gray-400">
|
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||||
<li>
|
Frequently Asked Questions
|
||||||
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
</h1>
|
||||||
</li>
|
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
||||||
<li>
|
Have questions? We've got answers. Here are some of the most common
|
||||||
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
things people ask about BentoPDF.
|
||||||
</li>
|
</p>
|
||||||
<li>
|
</section>
|
||||||
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="max-w-4xl mx-auto space-y-4">
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
||||||
<ul class="space-y-2 text-gray-400">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<li>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
Are my files safe and private?
|
||||||
</li>
|
</h3>
|
||||||
<li>
|
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
||||||
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
</summary>
|
||||||
</li>
|
<div class="mt-4 text-gray-400">
|
||||||
</ul>
|
<p>
|
||||||
</div>
|
<strong>Absolutely.</strong> This is the most important feature of
|
||||||
|
BentoPDF. All processing happens 100% locally in your web browser.
|
||||||
|
Your files are never uploaded to any server, which means we—or
|
||||||
|
anyone else—can never see them. Your privacy is guaranteed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div>
|
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
Is BentoPDF really free? What's the catch?
|
||||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
</h3>
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
||||||
<path fill-rule="evenodd"
|
</summary>
|
||||||
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"
|
<div class="mt-4 text-gray-400">
|
||||||
clip-rule="evenodd" />
|
<p>
|
||||||
</svg>
|
Yes, it's completely free, and there's no catch. There are no
|
||||||
</a>
|
hidden fees, no subscription plans, no usage limits, and no
|
||||||
<a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
premium-only features. We believe essential tools should be
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
accessible to everyone.
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
</p>
|
||||||
<path
|
</div>
|
||||||
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" />
|
</details>
|
||||||
</svg>
|
|
||||||
</a>
|
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
title="Instagram">
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="instagram"></i>
|
Do I need an internet connection to use the tools?
|
||||||
</a>
|
</h3>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
||||||
title="LinkedIn">
|
</summary>
|
||||||
<i data-lucide="linkedin"></i>
|
<div class="mt-4 text-gray-400">
|
||||||
</a>
|
<p>
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
|
After you load the website for the first time, most tools will
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
work completely offline! Because all the processing libraries are
|
||||||
<path
|
loaded into your browser, you can disconnect from the internet and
|
||||||
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" />
|
continue to merge, split, compress, and edit your PDFs securely.
|
||||||
</svg>
|
Some niche tools that require external data (like Markdown to PDF
|
||||||
</a>
|
with web images) may need a connection.
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
||||||
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
|
<h3 class="font-semibold text-white text-lg">
|
||||||
|
Are there any file size or usage limitations?
|
||||||
|
</h3>
|
||||||
|
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
|
<p>
|
||||||
|
No, we do not impose any artificial limits on file size, the
|
||||||
|
number of files, or how many times you can use a tool. The only
|
||||||
|
practical limitation is the processing power and memory of your
|
||||||
|
own computer, as very large or complex files may take longer to
|
||||||
|
process.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
||||||
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
|
<h3 class="font-semibold text-white text-lg">
|
||||||
|
Why did my PDF fail to process?
|
||||||
|
</h3>
|
||||||
|
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
|
<p>Failures are rare but can happen for a few reasons:</p>
|
||||||
|
<ul class="list-disc list-inside mt-2 space-y-1">
|
||||||
|
<li>
|
||||||
|
The PDF might be corrupted or not compliant with standard
|
||||||
|
specifications.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The file could be encrypted with a password you don't have.
|
||||||
|
Please use our Decrypt tool first.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The file might be a special "XFA" or dynamic form-based PDF,
|
||||||
|
which some of our tools cannot process.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
||||||
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
|
<h3 class="font-semibold text-white text-lg">
|
||||||
|
Do you track my activity on BentoPDF?
|
||||||
|
</h3>
|
||||||
|
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
|
<p>
|
||||||
|
We care about your privacy. BentoPDF does not track personal
|
||||||
|
information. We use
|
||||||
|
<a href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank"
|
||||||
|
rel="noopener noreferrer">Simple Analytics</a>
|
||||||
|
solely to see anonymous visit counts. This means we can know how
|
||||||
|
many users visit our site, but
|
||||||
|
<strong>we never know who you are</strong>. Simple Analytics is
|
||||||
|
fully GDPR-compliant and respects your privacy.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
||||||
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
|
<h3 class="font-semibold text-white text-lg">
|
||||||
|
What technology does BentoPDF use?
|
||||||
|
</h3>
|
||||||
|
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
|
<p>
|
||||||
|
BentoPDF is built on the power of modern web technologies. We
|
||||||
|
primarily use JavaScript libraries like
|
||||||
|
<strong>PDF-lib.js</strong> and <strong>PDFKit.js</strong> that
|
||||||
|
are compiled to run efficiently in your browser via WebAssembly.
|
||||||
|
This allows for powerful, server-like processing without your data
|
||||||
|
ever leaving your machine.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-divider mt-16"></div>
|
||||||
|
|
||||||
|
<section id="contact-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">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<a 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
|
||||||
|
</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">
|
||||||
|
© 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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
|
</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>
|
||||||
|
</body>
|
||||||
|
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
+149
-3
@@ -25,6 +25,7 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
<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>
|
||||||
@@ -189,8 +191,13 @@
|
|||||||
<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">
|
<span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2 gap-2">
|
||||||
<kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd>
|
<kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd>
|
||||||
|
<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>
|
||||||
@@ -252,6 +259,134 @@
|
|||||||
</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 -->
|
||||||
@@ -580,6 +715,13 @@
|
|||||||
</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">
|
||||||
@@ -594,7 +736,7 @@
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 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">1.6.2</span>
|
Version <span id="app-version"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -616,6 +758,9 @@
|
|||||||
<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>
|
||||||
@@ -636,7 +781,7 @@
|
|||||||
clip-rule="evenodd" />
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
<a href="https://discord.gg/AP2Y97juZT" 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
|
||||||
@@ -664,6 +809,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/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
@@ -0,0 +1,677 @@
|
|||||||
|
<!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">
|
||||||
|
© 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>
|
||||||
@@ -34,5 +34,7 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"version": "1.6.2",
|
"version": "1.7.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"version": "1.6.2",
|
"version": "1.7.3",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/cedarville-cursive": "^5.2.7",
|
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||||
|
|||||||
+4
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.1",
|
"version": "1.8.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,10 +16,12 @@
|
|||||||
"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 && npx serve dist -p 3000",
|
"serve:simple": "SIMPLE_MODE=true npm run build && npm run preview -- --port 3000",
|
||||||
|
"serve": "npm run build && npm run preview -- --port 3000",
|
||||||
"package": "node scripts/package-dist.js"
|
"package": "node scripts/package-dist.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+262
-302
@@ -1,319 +1,279 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Privacy Policy - BentoPDF</title>
|
|
||||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<head>
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<meta charset="UTF-8" />
|
||||||
<div class="container mx-auto px-4">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<div class="flex justify-between items-center h-16">
|
<title>Privacy Policy - BentoPDF</title>
|
||||||
<div
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
class="flex-shrink-0 flex items-center cursor-pointer"
|
</head>
|
||||||
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 -->
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="container mx-auto px-4">
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<div class="flex justify-between items-center h-16">
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||||
</div>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<button
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
id="mobile-menu-button"
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
type="button"
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
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"
|
<a href="./licensing.html" class="nav-link">Licensing</a>
|
||||||
aria-controls="mobile-menu"
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
aria-expanded="false"
|
</div>
|
||||||
>
|
|
||||||
<span class="sr-only">Open main menu</span>
|
<!-- Mobile Hamburger Button -->
|
||||||
<!-- Hamburger Icon -->
|
<div class="md:hidden flex items-center">
|
||||||
<svg
|
<button id="mobile-menu-button" type="button"
|
||||||
id="menu-icon"
|
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="block h-6 w-6"
|
aria-controls="mobile-menu" aria-expanded="false">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<span class="sr-only">Open main menu</span>
|
||||||
fill="none"
|
<!-- Hamburger Icon -->
|
||||||
viewBox="0 0 24 24"
|
<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"
|
stroke="currentColor" aria-hidden="true">
|
||||||
aria-hidden="true"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
>
|
</svg>
|
||||||
<path
|
<!-- Close Icon -->
|
||||||
stroke-linecap="round"
|
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
stroke-linejoin="round"
|
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
</svg>
|
||||||
/>
|
</button>
|
||||||
</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>
|
||||||
</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="index.html#tools-header" class="mobile-nav-link"
|
|
||||||
>All Tools</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="app" class="container mx-auto p-4 md:p-8">
|
|
||||||
<section class="max-w-4xl mx-auto py-12">
|
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-center text-white mb-4">
|
|
||||||
Privacy Policy
|
|
||||||
</h1>
|
|
||||||
<p class="text-center text-gray-500">
|
|
||||||
Last Updated: September 14, 2025
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="legal-content mt-12">
|
|
||||||
<h2>1. Our Commitment to Privacy</h2>
|
|
||||||
<p>
|
|
||||||
BentoPDF ("we", "us", "our") is fundamentally a privacy-focused
|
|
||||||
service. This Privacy Policy outlines our unwavering commitment to
|
|
||||||
protecting your privacy. Our core principle is simple:
|
|
||||||
<strong>your files are your files</strong>. We do not and cannot
|
|
||||||
view, access, store, or share your documents. All PDF processing
|
|
||||||
occurs entirely on your own computer, within your web browser
|
|
||||||
(client-side).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>1.1 The Client-Side Principle</h3>
|
|
||||||
<p>
|
|
||||||
Unlike other online PDF services, BentoPDF does not upload your
|
|
||||||
files to a server for processing. The tools you use are powered by
|
|
||||||
JavaScript and WebAssembly libraries that run directly on your
|
|
||||||
device. This means your data never leaves your computer, providing
|
|
||||||
you with the highest level of privacy and security.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>2. Information We Do Not Collect</h2>
|
|
||||||
<p>
|
|
||||||
Because of our client-side architecture, we are technically
|
|
||||||
incapable of collecting the following information:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
The content of your PDF files or any other documents you use with
|
|
||||||
our tools.
|
|
||||||
</li>
|
|
||||||
<li>Any personal data contained within your documents.</li>
|
|
||||||
<li>Filenames of your documents.</li>
|
|
||||||
<li>
|
|
||||||
Any derived information or metadata from your files, beyond what
|
|
||||||
is necessary for the tool to function during your active session
|
|
||||||
(and this is immediately discarded).
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>3. Information We May Collect (Non-Personal Data)</h2>
|
|
||||||
<p>
|
|
||||||
To improve our website and services, we may collect anonymous,
|
|
||||||
non-personally identifiable information. This type of data helps us
|
|
||||||
understand how users interact with our site, which tools are most
|
|
||||||
popular, and how we can improve the user experience. This includes:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<strong>Usage Analytics:</strong> Anonymized data such as which
|
|
||||||
tools are used, how often they are used, and which features are
|
|
||||||
accessed. This is aggregated and cannot be tied back to an
|
|
||||||
individual user or document.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Performance Data:</strong> Anonymized error reports or
|
|
||||||
performance metrics to help us identify and fix bugs. This data
|
|
||||||
contains no personal information or file content.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
We use privacy-respecting analytics platforms for this purpose.
|
|
||||||
Specifically, we use
|
|
||||||
<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
|
|
||||||
users visit our site, but
|
|
||||||
<strong
|
|
||||||
>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
|
|
||||||
cross-site profiling.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>4. Third-Party Libraries</h2>
|
|
||||||
<p>
|
|
||||||
BentoPDF is built using powerful, open-source libraries like
|
|
||||||
PDF-lib.js and PDF.js. These libraries are trusted by developers
|
|
||||||
worldwide and operate under the same client-side principle. While we
|
|
||||||
have vetted these libraries, we encourage you to review their
|
|
||||||
respective privacy policies for your own peace of mind.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>5. Security</h2>
|
|
||||||
<p>
|
|
||||||
Since your files are never transmitted over the internet to our
|
|
||||||
servers, you are protected from potential data breaches during
|
|
||||||
transit or storage on a server. The security of your documents is in
|
|
||||||
your hands and protected by the security of your own computer and
|
|
||||||
web browser.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>6. Children's Privacy</h2>
|
|
||||||
<p>
|
|
||||||
Our services are not directed at individuals under the age of 13. We
|
|
||||||
do not knowingly collect any personal information from children. If
|
|
||||||
you believe a child has provided us with personal information,
|
|
||||||
please contact us, and we will take steps to delete such
|
|
||||||
information.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>7. Changes to This Privacy Policy</h2>
|
|
||||||
<p>
|
|
||||||
We may update this Privacy Policy from time to time. We will notify
|
|
||||||
you of any changes by posting the new policy on this page and
|
|
||||||
updating the "Last Updated" date at the top. You are advised to
|
|
||||||
review this Privacy Policy periodically for any changes.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>8. Contact Us</h2>
|
|
||||||
<p>
|
|
||||||
If you have any questions about this Privacy Policy, please contact
|
|
||||||
us at
|
|
||||||
<a href="mailto:[email protected]">[email protected]</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div class="container mx-auto px-4">
|
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<div class="mb-8 md:mb-0">
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<a href="./licensing.html" class="mobile-nav-link">Licensing</a>
|
||||||
</div>
|
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||||
<p class="text-gray-400 text-sm">
|
</div>
|
||||||
© 2025 BentoPDF. All rights reserved.
|
</div>
|
||||||
</p>
|
</nav>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
|
||||||
Version <span id="app-version">1.6.2</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div id="app" class="container mx-auto p-4 md:p-8">
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<section class="max-w-4xl mx-auto py-12">
|
||||||
<ul class="space-y-2 text-gray-400">
|
<h1 class="text-4xl md:text-5xl font-bold text-center text-white mb-4">
|
||||||
<li>
|
Privacy Policy
|
||||||
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
</h1>
|
||||||
</li>
|
<p class="text-center text-gray-500">
|
||||||
<li>
|
Last Updated: September 14, 2025
|
||||||
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
</p>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="legal-content mt-12">
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h2>1. Our Commitment to Privacy</h2>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<p>
|
||||||
<li>
|
BentoPDF ("we", "us", "our") is fundamentally a privacy-focused
|
||||||
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
service. This Privacy Policy outlines our unwavering commitment to
|
||||||
</li>
|
protecting your privacy. Our core principle is simple:
|
||||||
<li>
|
<strong>your files are your files</strong>. We do not and cannot
|
||||||
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
view, access, store, or share your documents. All PDF processing
|
||||||
</li>
|
occurs entirely on your own computer, within your web browser
|
||||||
</ul>
|
(client-side).
|
||||||
</div>
|
</p>
|
||||||
|
|
||||||
<div>
|
<h3>1.1 The Client-Side Principle</h3>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<p>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
Unlike other online PDF services, BentoPDF does not upload your
|
||||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
files to a server for processing. The tools you use are powered by
|
||||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
JavaScript and WebAssembly libraries that run directly on your
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
device. This means your data never leaves your computer, providing
|
||||||
<path fill-rule="evenodd"
|
you with the highest level of privacy and security.
|
||||||
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"
|
</p>
|
||||||
clip-rule="evenodd" />
|
|
||||||
</svg>
|
<h2>2. Information We Do Not Collect</h2>
|
||||||
</a>
|
<p>
|
||||||
<a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
Because of our client-side architecture, we are technically
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
incapable of collecting the following information:
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
</p>
|
||||||
<path
|
<ul>
|
||||||
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" />
|
<li>
|
||||||
</svg>
|
The content of your PDF files or any other documents you use with
|
||||||
</a>
|
our tools.
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
</li>
|
||||||
title="Instagram">
|
<li>Any personal data contained within your documents.</li>
|
||||||
<i data-lucide="instagram"></i>
|
<li>Filenames of your documents.</li>
|
||||||
</a>
|
<li>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
Any derived information or metadata from your files, beyond what
|
||||||
title="LinkedIn">
|
is necessary for the tool to function during your active session
|
||||||
<i data-lucide="linkedin"></i>
|
(and this is immediately discarded).
|
||||||
</a>
|
</li>
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
|
</ul>
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
<h2>3. Information We May Collect (Non-Personal Data)</h2>
|
||||||
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" />
|
<p>
|
||||||
</svg>
|
To improve our website and services, we may collect anonymous,
|
||||||
</a>
|
non-personally identifiable information. This type of data helps us
|
||||||
</div>
|
understand how users interact with our site, which tools are most
|
||||||
|
popular, and how we can improve the user experience. This includes:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Usage Analytics:</strong> Anonymized data such as which
|
||||||
|
tools are used, how often they are used, and which features are
|
||||||
|
accessed. This is aggregated and cannot be tied back to an
|
||||||
|
individual user or document.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Performance Data:</strong> Anonymized error reports or
|
||||||
|
performance metrics to help us identify and fix bugs. This data
|
||||||
|
contains no personal information or file content.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
We use privacy-respecting analytics platforms for this purpose.
|
||||||
|
Specifically, we use
|
||||||
|
<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
|
||||||
|
users visit our site, but
|
||||||
|
<strong>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
|
||||||
|
cross-site profiling.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>4. Third-Party Libraries</h2>
|
||||||
|
<p>
|
||||||
|
BentoPDF is built using powerful, open-source libraries like
|
||||||
|
PDF-lib.js and PDF.js. These libraries are trusted by developers
|
||||||
|
worldwide and operate under the same client-side principle. While we
|
||||||
|
have vetted these libraries, we encourage you to review their
|
||||||
|
respective privacy policies for your own peace of mind.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>5. Security</h2>
|
||||||
|
<p>
|
||||||
|
Since your files are never transmitted over the internet to our
|
||||||
|
servers, you are protected from potential data breaches during
|
||||||
|
transit or storage on a server. The security of your documents is in
|
||||||
|
your hands and protected by the security of your own computer and
|
||||||
|
web browser.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>6. Children's Privacy</h2>
|
||||||
|
<p>
|
||||||
|
Our services are not directed at individuals under the age of 13. We
|
||||||
|
do not knowingly collect any personal information from children. If
|
||||||
|
you believe a child has provided us with personal information,
|
||||||
|
please contact us, and we will take steps to delete such
|
||||||
|
information.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>7. Changes to This Privacy Policy</h2>
|
||||||
|
<p>
|
||||||
|
We may update this Privacy Policy from time to time. We will notify
|
||||||
|
you of any changes by posting the new policy on this page and
|
||||||
|
updating the "Last Updated" date at the top. You are advised to
|
||||||
|
review this Privacy Policy periodically for any changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>8. Contact Us</h2>
|
||||||
|
<p>
|
||||||
|
If you have any questions about this Privacy Policy, please contact
|
||||||
|
us at
|
||||||
|
<a href="mailto:[email protected]">[email protected]</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
© 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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
</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/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
@@ -12372,7 +12372,10 @@ 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") ?? AppOptions.get("defaultUrl");
|
file = params.get("file");
|
||||||
|
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";
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
viewer: document.getElementById('viewer'),
|
viewer: document.getElementById('viewer'),
|
||||||
eventBus,
|
eventBus,
|
||||||
linkService,
|
linkService,
|
||||||
annotationEditorMode: 0,
|
annotationEditorMode: 1,
|
||||||
enableScripting: true,
|
enableScripting: true,
|
||||||
renderer: 'canvas'
|
renderer: 'canvas'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1512,7 +1512,7 @@ class BaseExternalServices {
|
|||||||
class BasePreferences {
|
class BasePreferences {
|
||||||
#defaults = Object.freeze({
|
#defaults = Object.freeze({
|
||||||
altTextLearnMoreUrl: "",
|
altTextLearnMoreUrl: "",
|
||||||
annotationEditorMode: 0,
|
annotationEditorMode: 1,
|
||||||
annotationMode: 2,
|
annotationMode: 2,
|
||||||
capCanvasAreaFactor: 200,
|
capCanvasAreaFactor: 200,
|
||||||
commentLearnMoreUrl: "",
|
commentLearnMoreUrl: "",
|
||||||
@@ -16429,7 +16429,10 @@ 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") ?? AppOptions.get("defaultUrl");
|
file = params.get("file");
|
||||||
|
if (!file) {
|
||||||
|
file = "";
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
file = new URL(decodeURIComponent(file)).href;
|
file = new URL(decodeURIComponent(file)).href;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
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 || ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
+9
-5
@@ -75,20 +75,24 @@ function main() {
|
|||||||
const newVersion = updateVersion(type);
|
const newVersion = updateVersion(type);
|
||||||
console.log(`📦 Updated version to ${newVersion}`);
|
console.log(`📦 Updated version to ${newVersion}`);
|
||||||
|
|
||||||
// 2. Add and commit changes
|
// 2. Update version in HTML files
|
||||||
execSync('git add package.json', { stdio: 'inherit' });
|
console.log(`📝 Updating version in HTML files...`);
|
||||||
|
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`);
|
||||||
|
|
||||||
// 3. Create git tag
|
// 4. Create git tag
|
||||||
const tagName = createGitTag(newVersion);
|
const tagName = createGitTag(newVersion);
|
||||||
|
|
||||||
// 4. Build and package the distribution files
|
// 5. 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`);
|
||||||
|
|
||||||
// 5. Push everything to main
|
// 6. 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' });
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/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
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+74
-6
@@ -109,7 +109,7 @@ input[type='file']::file-selector-button {
|
|||||||
border: 2px dashed #4f46e5;
|
border: 2px dashed #4f46e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#embed-pdf-container > div {
|
#embed-pdf-container>div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ input[type='file']::file-selector-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-thumbnail,
|
.page-thumbnail,
|
||||||
#file-list > li {
|
#file-list>li {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,19 +495,19 @@ footer a {
|
|||||||
color: rgb(165 180 252);
|
color: rgb(165 180 252);
|
||||||
}
|
}
|
||||||
|
|
||||||
details > summary {
|
details>summary {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
details > summary::-webkit-details-marker {
|
details>summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
details > summary .icon {
|
details>summary .icon {
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open] > summary .icon {
|
details[open]>summary .icon {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,3 +521,71 @@ 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;
|
||||||
|
}
|
||||||
+26
-7
@@ -1,9 +1,29 @@
|
|||||||
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';
|
||||||
|
|
||||||
const editorState = {
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -41,7 +61,6 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -185,12 +204,10 @@ 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();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
editorState.pdf = await getPDFDocument({ data: pdfData }).promise;
|
||||||
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 = '';
|
||||||
@@ -256,11 +273,13 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
|
|
||||||
if (toolId === 'crop') {
|
if (toolId === 'crop') {
|
||||||
document.getElementById('zoom-in-btn').onclick = () => {
|
document.getElementById('zoom-in-btn').onclick = () => {
|
||||||
editorState.scale += 0.25;
|
if (typeof editorState.scale === 'number') {
|
||||||
|
editorState.scale += 0.25;
|
||||||
|
}
|
||||||
renderPage(editorState.currentPageNum);
|
renderPage(editorState.currentPageNum);
|
||||||
};
|
};
|
||||||
document.getElementById('zoom-out-btn').onclick = () => {
|
document.getElementById('zoom-out-btn').onclick = () => {
|
||||||
if (editorState.scale > 0.25) {
|
if (typeof editorState.scale === 'number' && editorState.scale > 0.25) {
|
||||||
editorState.scale -= 0.25;
|
editorState.scale -= 0.25;
|
||||||
renderPage(editorState.currentPageNum);
|
renderPage(editorState.currentPageNum);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,12 @@ 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',
|
||||||
@@ -176,7 +182,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: 'Combine various images into one PDF.',
|
subtitle: 'Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'jpg-to-pdf',
|
id: 'jpg-to-pdf',
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// 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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
+132
-11
@@ -7,7 +7,7 @@ import {
|
|||||||
renderFileDisplay,
|
renderFileDisplay,
|
||||||
switchView,
|
switchView,
|
||||||
} from '../ui.js';
|
} from '../ui.js';
|
||||||
import { formatIsoDate, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { formatIsoDate, readFileAsArrayBuffer, getPDFDocument } 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,6 +21,26 @@ 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 {
|
||||||
@@ -101,6 +121,12 @@ 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'
|
||||||
);
|
);
|
||||||
@@ -113,11 +139,15 @@ 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 currentRotation = parseInt(
|
const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0');
|
||||||
(item as HTMLElement).dataset.rotation || '0'
|
const newRotation = rotationState[pageIndex];
|
||||||
);
|
|
||||||
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) {
|
||||||
@@ -144,7 +174,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 pdfjsLib.getDocument({
|
const pdfjsDoc = await getPDFDocument({
|
||||||
data: pdfBytes as ArrayBuffer,
|
data: pdfBytes as ArrayBuffer,
|
||||||
}).promise;
|
}).promise;
|
||||||
const [metadataResult, fieldObjects] = await Promise.all([
|
const [metadataResult, fieldObjects] = await Promise.all([
|
||||||
@@ -449,6 +479,43 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -468,6 +535,8 @@ 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) => {
|
||||||
@@ -502,6 +571,7 @@ 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');
|
||||||
@@ -526,9 +596,27 @@ 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) => {
|
||||||
const url = URL.createObjectURL(file);
|
if (!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;
|
||||||
@@ -550,10 +638,28 @@ async function handleMultiFileUpload(toolId) {
|
|||||||
imageList.appendChild(li);
|
imageList.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|
||||||
Sortable.create(imageList);
|
const syncStateWithDOM = () => {
|
||||||
// Show image-to-pdf options and wire up slider text
|
const domOrder = Array.from(imageList.querySelectorAll('li')).map(li => li.dataset.fileName);
|
||||||
|
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) {
|
if (opts && opts.classList.contains('hidden')) {
|
||||||
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');
|
||||||
@@ -617,6 +723,18 @@ 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 {
|
||||||
@@ -731,6 +849,9 @@ 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 = '';
|
||||||
|
|||||||
+114
-27
@@ -1,10 +1,40 @@
|
|||||||
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.pdfDoc) {
|
if (!state.files || state.files.length === 0) {
|
||||||
showAlert('Error', 'Main PDF is not loaded.');
|
showAlert('Error', 'Main PDF is not loaded.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -13,39 +43,57 @@ 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 pdfDoc = state.pdfDoc;
|
const pdfFile = state.files[0];
|
||||||
|
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(`Attaching ${file.name} (${i + 1}/${attachments.length})...`);
|
showLoader(`Reading ${file.name} (${i + 1}/${attachments.length})...`);
|
||||||
|
|
||||||
const fileBytes = await readFileAsArrayBuffer(file);
|
const fileBuffer = (await readFileAsArrayBuffer(file)) as ArrayBuffer;
|
||||||
|
attachmentBuffers.push(fileBuffer);
|
||||||
await pdfDoc.attach(fileBytes as ArrayBuffer, file.name, {
|
attachmentNames.push(file.name);
|
||||||
mimeType: file.type || 'application/octet-stream',
|
|
||||||
description: `Attached file: ${file.name}`,
|
|
||||||
creationDate: new Date(),
|
|
||||||
modificationDate: new Date(file.lastModified),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
showLoader('Attaching files to PDF...');
|
||||||
downloadFile(
|
|
||||||
new Blob([pdfBytes], { type: 'application/pdf' }),
|
const message = {
|
||||||
`attached-${state.files[0].name}`
|
command: 'add-attachments',
|
||||||
);
|
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);
|
||||||
showAlert('Error', `Failed to attach files: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||||
clearAttachments();
|
clearAttachments();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +107,8 @@ 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 = '';
|
||||||
@@ -66,14 +116,20 @@ 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;
|
||||||
|
if (documentRadio) {
|
||||||
|
documentRadio.checked = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isSetup = false; // Prevent duplicate setup
|
|
||||||
|
|
||||||
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'
|
||||||
@@ -82,14 +138,29 @@ 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) {
|
||||||
@@ -114,6 +185,10 @@ 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 {
|
||||||
@@ -121,5 +196,17 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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
|
||||||
@@ -58,11 +59,23 @@ 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
|
||||||
|
|
||||||
iframe.src = `/pdfjs-annotation-viewer/web/viewer.html?file=${encodeURIComponent(currentBlobUrl)}`
|
const viewerUrl = new URL('/pdfjs-annotation-viewer/web/viewer.html', window.location.origin)
|
||||||
|
iframe.src = `${viewerUrl.toString()}?file=${encodeURIComponent(currentBlobUrl)}`
|
||||||
|
|
||||||
iframe.addEventListener('load', () => {
|
iframe.addEventListener('load', () => {
|
||||||
setupAnnotationViewer(iframe)
|
setupAnnotationViewer(iframe)
|
||||||
@@ -85,10 +98,14 @@ function setupAnnotationViewer(iframe: HTMLIFrameElement) {
|
|||||||
await app.initializedPromise
|
await app.initializedPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
const stampBtn = doc.getElementById('editorStamp') as HTMLButtonElement | null
|
const eventBus = app?.eventBus
|
||||||
if (stampBtn) {
|
if (eventBus && typeof eventBus._on === 'function') {
|
||||||
stampBtn.classList.remove('hidden')
|
eventBus._on('annotationeditoruimanager', () => {
|
||||||
stampBtn.disabled = false
|
try {
|
||||||
|
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
|
||||||
@@ -158,3 +175,5 @@ if (backToToolsBtn) {
|
|||||||
window.location.href = '/'
|
window.location.href = '/'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeGlobalShortcuts()
|
||||||
@@ -3,6 +3,7 @@ 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';
|
||||||
|
|
||||||
@@ -40,9 +41,9 @@ export function setupWatermarkUI() {
|
|||||||
opacitySliderText.addEventListener(
|
opacitySliderText.addEventListener(
|
||||||
'input',
|
'input',
|
||||||
() =>
|
() =>
|
||||||
(opacityValueText.textContent = (
|
(opacityValueText.textContent = (
|
||||||
opacitySliderText as HTMLInputElement
|
opacitySliderText as HTMLInputElement
|
||||||
).value)
|
).value)
|
||||||
);
|
);
|
||||||
|
|
||||||
angleSliderText.addEventListener(
|
angleSliderText.addEventListener(
|
||||||
@@ -59,17 +60,17 @@ export function setupWatermarkUI() {
|
|||||||
opacitySliderImage.addEventListener(
|
opacitySliderImage.addEventListener(
|
||||||
'input',
|
'input',
|
||||||
() =>
|
() =>
|
||||||
(opacityValueImage.textContent = (
|
(opacityValueImage.textContent = (
|
||||||
opacitySliderImage as HTMLInputElement
|
opacitySliderImage as HTMLInputElement
|
||||||
).value)
|
).value)
|
||||||
);
|
);
|
||||||
|
|
||||||
angleSliderImage.addEventListener(
|
angleSliderImage.addEventListener(
|
||||||
'input',
|
'input',
|
||||||
() =>
|
() =>
|
||||||
(angleValueImage.textContent = (
|
(angleValueImage.textContent = (
|
||||||
angleSliderImage as HTMLInputElement
|
angleSliderImage as HTMLInputElement
|
||||||
).value)
|
).value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,14 +176,7 @@ export async function addWatermark() {
|
|||||||
'watermarked.pdf'
|
'watermarked.pdf'
|
||||||
);
|
);
|
||||||
|
|
||||||
const toolid = state.activeTool;
|
resetAndReloadTool();
|
||||||
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(
|
||||||
|
|||||||
@@ -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,6 +453,8 @@ 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');
|
||||||
@@ -712,8 +714,10 @@ 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-blue-50', 'text-blue-600');
|
showViewerBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
showBookmarksBtn.classList.remove('bg-blue-50', 'text-blue-600');
|
showViewerBtn.classList.add('text-gray-300');
|
||||||
|
showBookmarksBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
|
showBookmarksBtn.classList.add('text-gray-300');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,15 +726,19 @@ 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-blue-50', 'text-blue-600');
|
showViewerBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
showBookmarksBtn.classList.remove('bg-blue-50', 'text-blue-600');
|
showViewerBtn.classList.remove('text-gray-300');
|
||||||
|
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-blue-50', 'text-blue-600');
|
showBookmarksBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
showViewerBtn.classList.remove('bg-blue-50', 'text-blue-600');
|
showBookmarksBtn.classList.remove('text-gray-300');
|
||||||
|
showViewerBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
|
showViewerBtn.classList.add('text-gray-300');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dropdown toggles
|
// Dropdown toggles
|
||||||
@@ -860,8 +868,10 @@ 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-blue-50', 'text-blue-600');
|
showViewerBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
showBookmarksBtn.classList.remove('bg-blue-50', 'text-blue-600');
|
showViewerBtn.classList.remove('text-gray-300');
|
||||||
|
showBookmarksBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
|
showBookmarksBtn.classList.add('text-gray-300');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
@@ -1030,7 +1040,7 @@ async function loadPDF(e) {
|
|||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
originalFileName = file.name.replace('.pdf', '');
|
originalFileName = file.name.replace('.pdf', '');
|
||||||
filenameDisplay.textContent = originalFileName;
|
filenameDisplay.textContent = truncateFilename(file.name);
|
||||||
renderFileDisplay(file);
|
renderFileDisplay(file);
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
|
||||||
@@ -1043,7 +1053,7 @@ async function loadPDF(e) {
|
|||||||
|
|
||||||
pdfLibDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
|
pdfLibDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
|
||||||
|
|
||||||
const loadingTask = pdfjsLib.getDocument({
|
const loadingTask = getPDFDocument({
|
||||||
data: new Uint8Array(arrayBuffer),
|
data: new Uint8Array(arrayBuffer),
|
||||||
});
|
});
|
||||||
pdfJsDoc = await loadingTask.promise;
|
pdfJsDoc = await loadingTask.promise;
|
||||||
@@ -1898,45 +1908,64 @@ async function extractExistingBookmarks(doc) {
|
|||||||
console.error('Error building named destinations:', e);
|
console.error('Error building named destinations:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPageIndex(pageRef) {
|
function findPageIndex(pageRef, resolvedPageRef = null) {
|
||||||
if (!pageRef) return 0;
|
if (!pageRef) return 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resolved = resolveRef(pageRef);
|
// Method 1: Try the resolved page ref first (if provided)
|
||||||
|
if (resolvedPageRef) {
|
||||||
if (pageRef.numberValue !== undefined) {
|
// Check if resolved is a page dictionary - compare to each page's dictionary
|
||||||
const numericIndex = pageRef.numberValue | 0;
|
|
||||||
if (numericIndex >= 0 && numericIndex < pages.length)
|
|
||||||
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++) {
|
for (let i = 0; i < pages.length; i++) {
|
||||||
const pageDict = doc.context.lookup(pages[i].ref);
|
const pageDict = doc.context.lookup(pages[i].ref);
|
||||||
if (pageDict === resolved) return i;
|
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);
|
||||||
|
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) {
|
||||||
|
const numericIndex = pageRef.numberValue | 0;
|
||||||
|
if (numericIndex >= 0 && numericIndex < pages.length) {
|
||||||
|
return numericIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error finding page:', e);
|
console.error('Error finding page index:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: keep current behavior but log for diagnostics
|
// If we couldn't find a match, return 0
|
||||||
console.warn('Falling back to page 0 for destination');
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1959,7 +1988,24 @@ 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)) {
|
||||||
dest = namedDests.get(name);
|
const namedDest = 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);
|
||||||
@@ -1974,7 +2020,9 @@ async function extractExistingBookmarks(doc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveRef(dest);
|
// dest is typically an array like [pageRef, /XYZ, x, y, zoom]
|
||||||
|
// Resolving it would corrupt the array structure
|
||||||
|
return dest;
|
||||||
}
|
}
|
||||||
|
|
||||||
function traverse(item) {
|
function traverse(item) {
|
||||||
@@ -1994,7 +2042,8 @@ async function extractExistingBookmarks(doc) {
|
|||||||
|
|
||||||
if (dest && dest.array) {
|
if (dest && dest.array) {
|
||||||
const pageRef = dest.array[0];
|
const pageRef = dest.array[0];
|
||||||
pageIndex = findPageIndex(pageRef);
|
const resolvedPageRef = resolveRef(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]);
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ 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;
|
||||||
@@ -16,29 +20,25 @@ async function updateTextColorPreview() {
|
|||||||
isRenderingPreview = true;
|
isRenderingPreview = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const textColorCanvas = document.getElementById('text-color-canvas');
|
const textColorCanvas = document.getElementById('text-color-canvas') as HTMLCanvasElement;
|
||||||
if (!textColorCanvas) return;
|
if (!textColorCanvas) return;
|
||||||
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdf = await getPDFDocument(
|
||||||
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 }).promise;
|
await page.render({ canvasContext: context, viewport, canvas: textColorCanvas }).promise;
|
||||||
const imageData = context.getImageData(
|
const imageData = context.getImageData(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
(textColorCanvas as HTMLCanvasElement).width,
|
textColorCanvas.width,
|
||||||
(textColorCanvas as HTMLCanvasElement).height
|
textColorCanvas.height
|
||||||
);
|
);
|
||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
const colorHex = (
|
const colorHex = (
|
||||||
@@ -78,21 +78,19 @@ export async function setupTextColorTool() {
|
|||||||
renderTimeout = setTimeout(updateTextColorPreview, 250);
|
renderTimeout = setTimeout(updateTextColorPreview, 250);
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdf = await getPDFDocument(
|
||||||
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 });
|
||||||
|
|
||||||
// @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).width = viewport.width;
|
||||||
originalCanvas.width = viewport.width;
|
(originalCanvas as HTMLCanvasElement).height = viewport.height;
|
||||||
// @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();
|
||||||
}
|
}
|
||||||
@@ -103,16 +101,14 @@ export async function changeTextColor() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const colorHex = (document.getElementById('text-color-input') as HTMLInputElement).value;
|
||||||
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();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdf = await getPDFDocument(
|
||||||
const pdf = await pdfjsLib.getDocument(
|
|
||||||
await readFileAsArrayBuffer(state.files[0])
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
).promise;
|
).promise;
|
||||||
|
|
||||||
@@ -126,7 +122,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 }).promise;
|
await page.render({ canvasContext: context, viewport, canvas }).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;
|
||||||
@@ -144,16 +140,15 @@ export async function changeTextColor() {
|
|||||||
}
|
}
|
||||||
context.putImageData(imageData, 0, 0);
|
context.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
const pngImageBytes = await new Promise((resolve) =>
|
const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
reader.readAsArrayBuffer(blob!);
|
||||||
reader.readAsArrayBuffer(blob);
|
|
||||||
}, 'image/png')
|
}, 'image/png')
|
||||||
);
|
);
|
||||||
|
|
||||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
const pngImage = await newPdfDoc.embedPng(pngImageBytes);
|
||||||
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,
|
||||||
|
|||||||
@@ -1,67 +1,208 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb, getPDFDocument } 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 sourcePages = sourceDoc.getPages();
|
|
||||||
|
|
||||||
|
const pdfBytes = await sourceDoc.save();
|
||||||
|
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||||
|
|
||||||
|
const sourcePages = sourceDoc.getPages();
|
||||||
let maxWidth = 0;
|
let maxWidth = 0;
|
||||||
|
let maxHeight = 0;
|
||||||
|
let totalWidth = 0;
|
||||||
let totalHeight = 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;
|
|
||||||
|
|
||||||
const newPage = newDoc.addPage([maxWidth, totalHeight]);
|
let finalWidth, finalHeight;
|
||||||
|
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: maxWidth,
|
width: finalWidth,
|
||||||
height: totalHeight,
|
height: finalHeight,
|
||||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentY = totalHeight;
|
let currentX = 0;
|
||||||
|
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);
|
|
||||||
|
|
||||||
currentY -= height;
|
try {
|
||||||
const x = (maxWidth - width) / 2;
|
const page = await pdfjsDoc.getPage(i + 1);
|
||||||
|
const scale = 2.0;
|
||||||
|
const viewport = page.getViewport({ scale });
|
||||||
|
|
||||||
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
const context = canvas.getContext('2d')!;
|
||||||
|
|
||||||
if (addSeparator && i < sourcePages.length - 1) {
|
await page.render({
|
||||||
const lineY = currentY - spacing / 2;
|
canvasContext: context,
|
||||||
newPage.drawLine({
|
viewport,
|
||||||
start: { x: 0, y: lineY },
|
canvas
|
||||||
end: { x: maxWidth, y: lineY },
|
}).promise;
|
||||||
thickness: 0.5,
|
|
||||||
color: rgb(0.8, 0.8, 0.8),
|
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;
|
||||||
|
const x = (finalWidth - width) / 2; // Center horizontally
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
} 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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentY -= spacing;
|
// Draw separator line
|
||||||
|
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;
|
||||||
|
newPage.drawLine({
|
||||||
|
start: { x: 0, y: lineY },
|
||||||
|
end: { x: finalWidth, y: lineY },
|
||||||
|
thickness: separatorThickness,
|
||||||
|
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
|
||||||
|
});
|
||||||
|
currentY -= spacing;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (orientation === 'horizontal') {
|
||||||
|
currentX += width + spacing;
|
||||||
|
} else {
|
||||||
|
currentY -= spacing;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
const newPdfBytes = await newDoc.save();
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { readFileAsArrayBuffer, getPDFDocument } 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,
|
||||||
@@ -122,8 +126,7 @@ 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);
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
state[docKey] = await getPDFDocument(pdfBytes).promise;
|
||||||
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');
|
||||||
|
|||||||
+10
-13
@@ -3,12 +3,14 @@ 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';
|
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();
|
||||||
|
|
||||||
function dataUrlToBytes(dataUrl: any) {
|
function dataUrlToBytes(dataUrl: any) {
|
||||||
const base64 = dataUrl.split(',')[1];
|
const base64 = dataUrl.split(',')[1];
|
||||||
const binaryString = atob(base64);
|
const binaryString = atob(base64);
|
||||||
@@ -70,8 +72,8 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
const bitsPerComponent =
|
const bitsPerComponent =
|
||||||
stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
||||||
? (
|
? (
|
||||||
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
|
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
|
||||||
).asNumber()
|
).asNumber()
|
||||||
: 8;
|
: 8;
|
||||||
|
|
||||||
if (width > 0 && height > 0) {
|
if (width > 0 && height > 0) {
|
||||||
@@ -183,12 +185,7 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
const pdfJsDoc = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
'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++) {
|
||||||
@@ -324,13 +321,13 @@ export async function compress() {
|
|||||||
showAlert(
|
showAlert(
|
||||||
'Compression Complete',
|
'Compression Complete',
|
||||||
`Method: **${usedMethod}**. ` +
|
`Method: **${usedMethod}**. ` +
|
||||||
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
|
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Compression Finished',
|
'Compression Finished',
|
||||||
`Method: **${usedMethod}**. ` +
|
`Method: **${usedMethod}**. ` +
|
||||||
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
|
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
|
||||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||||
'warning'
|
'warning'
|
||||||
);
|
);
|
||||||
@@ -384,13 +381,13 @@ export async function compress() {
|
|||||||
showAlert(
|
showAlert(
|
||||||
'Compression Complete',
|
'Compression Complete',
|
||||||
`Compressed ${state.files.length} PDF(s). ` +
|
`Compressed ${state.files.length} PDF(s). ` +
|
||||||
`Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${totalSavingsPercent}%).`
|
`Total size reduced from ${formatBytes(totalOriginalSize)} to ${formatBytes(totalCompressedSize)} (Saved ${totalSavingsPercent}%).`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Compression Finished',
|
'Compression Finished',
|
||||||
`Compressed ${state.files.length} PDF(s). ` +
|
`Compressed ${state.files.length} PDF(s). ` +
|
||||||
`Total size: ${formatBytes(totalCompressedSize)}.`
|
`Total size: ${formatBytes(totalCompressedSize)}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+128
-130
@@ -1,10 +1,12 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } 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,
|
||||||
@@ -129,45 +131,44 @@ 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) {
|
||||||
// @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 pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
|
||||||
const page = pdfToModify.getPages()[pageNum - 1];
|
const viewport = pdfJsPage.getViewport({ scale: 1 });
|
||||||
const { width: pageWidth, height: pageHeight } = page.getSize();
|
|
||||||
const rotation = page.getRotation().angle;
|
|
||||||
const crop = cropData[pageNum];
|
const crop = cropData[pageNum];
|
||||||
|
|
||||||
const visualPdfWidth = pageWidth * crop.width;
|
// Man I hate doing math
|
||||||
const visualPdfHeight = pageHeight * crop.height;
|
// Calculate visual crop rectangle in viewport pixels
|
||||||
const visualPdfX = pageWidth * crop.x;
|
const cropX = viewport.width * crop.x;
|
||||||
const visualPdfY = pageHeight * crop.y;
|
const cropY = viewport.height * crop.y;
|
||||||
|
const cropW = viewport.width * crop.width;
|
||||||
|
const cropH = viewport.height * crop.height;
|
||||||
|
|
||||||
let finalX, finalY, finalWidth, finalHeight;
|
// Define the 4 corners of the crop rectangle in visual coordinates (Top-Left origin)
|
||||||
switch (rotation) {
|
const visualCorners = [
|
||||||
case 90:
|
{ x: cropX, y: cropY }, // TL
|
||||||
finalX = visualPdfY;
|
{ x: cropX + cropW, y: cropY }, // TR
|
||||||
finalY = pageWidth - visualPdfX - visualPdfWidth;
|
{ x: cropX + cropW, y: cropY + cropH }, // BR
|
||||||
finalWidth = visualPdfHeight;
|
{ x: cropX, y: cropY + cropH }, // BL
|
||||||
finalHeight = visualPdfWidth;
|
];
|
||||||
break;
|
|
||||||
case 180:
|
// This handles rotation, media box offsets, and coordinate system flips automatically
|
||||||
finalX = pageWidth - visualPdfX - visualPdfWidth;
|
const pdfCorners = visualCorners.map(p => {
|
||||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
return viewport.convertToPdfPoint(p.x, p.y);
|
||||||
finalWidth = visualPdfWidth;
|
});
|
||||||
finalHeight = visualPdfHeight;
|
|
||||||
break;
|
// Find the bounding box of the converted points in PDF coordinates
|
||||||
case 270:
|
// convertToPdfPoint returns [x, y] arrays
|
||||||
finalX = pageHeight - visualPdfY - visualPdfHeight;
|
const pdfXs = pdfCorners.map(p => p[0]);
|
||||||
finalY = visualPdfX;
|
const pdfYs = pdfCorners.map(p => p[1]);
|
||||||
finalWidth = visualPdfHeight;
|
|
||||||
finalHeight = visualPdfWidth;
|
const minX = Math.min(...pdfXs);
|
||||||
break;
|
const maxX = Math.max(...pdfXs);
|
||||||
default:
|
const minY = Math.min(...pdfYs);
|
||||||
finalX = visualPdfX;
|
const maxY = Math.max(...pdfYs);
|
||||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
|
||||||
finalWidth = visualPdfWidth;
|
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||||
finalHeight = visualPdfHeight;
|
const page = pdfToModify.getPages()[pageNum - 1];
|
||||||
break;
|
page.setCropBox(minX, minY, maxX - minX, maxY - minY);
|
||||||
}
|
|
||||||
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,107 +243,104 @@ export async function setupCropperTool() {
|
|||||||
if (state.files.length === 0) return;
|
if (state.files.length === 0) return;
|
||||||
|
|
||||||
// Clear pageCrops on new file upload
|
// Clear pageCrops on new file upload
|
||||||
cropperState.pageCrops = {};
|
|
||||||
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
|
|
||||||
cropperState.originalPdfBytes = arrayBuffer;
|
|
||||||
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
|
||||||
import.meta.url
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
|
// Clear pageCrops on new file upload
|
||||||
|
cropperState.pageCrops = {};
|
||||||
|
|
||||||
|
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
|
||||||
|
cropperState.originalPdfBytes = arrayBuffer;
|
||||||
|
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
|
||||||
|
const loadingTask = getPDFDocument({ 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);
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById('prev-page')
|
|
||||||
.addEventListener('click', () => changePage(-1));
|
|
||||||
document
|
|
||||||
.getElementById('next-page')
|
|
||||||
.addEventListener('click', () => changePage(1));
|
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById('crop-button')
|
|
||||||
.addEventListener('click', async () => {
|
|
||||||
// Get the last known crop from the active page before processing
|
|
||||||
saveCurrentCrop();
|
|
||||||
|
|
||||||
const isDestructive = (
|
|
||||||
document.getElementById('destructive-crop-toggle') as HTMLInputElement
|
|
||||||
).checked;
|
|
||||||
const isApplyToAll = (
|
|
||||||
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
|
||||||
).checked;
|
|
||||||
|
|
||||||
let finalCropData = {};
|
|
||||||
if (isApplyToAll) {
|
|
||||||
const currentCrop =
|
|
||||||
cropperState.pageCrops[cropperState.currentPageNum];
|
|
||||||
if (!currentCrop) {
|
|
||||||
showAlert('No Crop Area', 'Please select an area to crop first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Apply the active page's crop to all pages
|
|
||||||
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
|
|
||||||
finalCropData[i] = currentCrop;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If not applying to all, only process pages with saved crops
|
|
||||||
finalCropData = Object.keys(cropperState.pageCrops).reduce(
|
|
||||||
(obj, key) => {
|
|
||||||
obj[key] = cropperState.pageCrops[key];
|
|
||||||
return obj;
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(finalCropData).length === 0) {
|
|
||||||
showAlert(
|
|
||||||
'No Crop Area',
|
|
||||||
'Please select an area on at least one page to crop.'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Applying crop...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
let finalPdfBytes;
|
|
||||||
if (isDestructive) {
|
|
||||||
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
|
||||||
finalPdfBytes = await newPdfDoc.save();
|
|
||||||
} else {
|
|
||||||
const pdfToModify = await PDFLibDocument.load(
|
|
||||||
cropperState.originalPdfBytes
|
|
||||||
);
|
|
||||||
await performMetadataCrop(pdfToModify, finalCropData);
|
|
||||||
finalPdfBytes = await pdfToModify.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = isDestructive
|
|
||||||
? 'flattened_crop.pdf'
|
|
||||||
: 'standard_crop.pdf';
|
|
||||||
downloadFile(
|
|
||||||
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
|
||||||
fileName
|
|
||||||
);
|
|
||||||
showAlert('Success', 'Crop complete! Your download has started.');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'An error occurred during cropping.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting up cropper tool:', error);
|
console.error('Error setting up cropper tool:', error);
|
||||||
showAlert('Error', 'Failed to load PDF for cropping.');
|
showAlert('Error', 'Failed to load PDF for cropping.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('prev-page')
|
||||||
|
.addEventListener('click', () => changePage(-1));
|
||||||
|
document
|
||||||
|
.getElementById('next-page')
|
||||||
|
.addEventListener('click', () => changePage(1));
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('crop-button')
|
||||||
|
.addEventListener('click', async () => {
|
||||||
|
// Get the last known crop from the active page before processing
|
||||||
|
saveCurrentCrop();
|
||||||
|
|
||||||
|
const isDestructive = (
|
||||||
|
document.getElementById('destructive-crop-toggle') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const isApplyToAll = (
|
||||||
|
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
|
||||||
|
let finalCropData = {};
|
||||||
|
if (isApplyToAll) {
|
||||||
|
const currentCrop =
|
||||||
|
cropperState.pageCrops[cropperState.currentPageNum];
|
||||||
|
if (!currentCrop) {
|
||||||
|
showAlert('No Crop Area', 'Please select an area to crop first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Apply the active page's crop to all pages
|
||||||
|
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
|
||||||
|
finalCropData[i] = currentCrop;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If not applying to all, only process pages with saved crops
|
||||||
|
finalCropData = Object.keys(cropperState.pageCrops).reduce(
|
||||||
|
(obj, key) => {
|
||||||
|
obj[key] = cropperState.pageCrops[key];
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(finalCropData).length === 0) {
|
||||||
|
showAlert(
|
||||||
|
'No Crop Area',
|
||||||
|
'Please select an area on at least one page to crop.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Applying crop...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let finalPdfBytes;
|
||||||
|
if (isDestructive) {
|
||||||
|
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
||||||
|
finalPdfBytes = await newPdfDoc.save();
|
||||||
|
} else {
|
||||||
|
const pdfToModify = await PDFLibDocument.load(
|
||||||
|
cropperState.originalPdfBytes
|
||||||
|
);
|
||||||
|
await performMetadataCrop(pdfToModify, finalCropData);
|
||||||
|
finalPdfBytes = await pdfToModify.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = isDestructive
|
||||||
|
? 'flattened_crop.pdf'
|
||||||
|
: 'standard_crop.pdf';
|
||||||
|
downloadFile(
|
||||||
|
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
||||||
|
fileName
|
||||||
|
);
|
||||||
|
showAlert('Success', 'Crop complete! Your download has started.');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'An error occurred during cropping.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile, getPDFDocument } 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: {},
|
||||||
@@ -82,27 +86,22 @@ 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();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise;
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
|
||||||
|
|
||||||
grid.textContent = '';
|
grid.textContent = '';
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
// Function to create wrapper element for each page
|
||||||
const page = await pdfjsDoc.getPage(i);
|
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
||||||
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 = i - 1;
|
wrapper.dataset.originalPageIndex = pageNumber - 1;
|
||||||
|
|
||||||
const imgContainer = document.createElement('div');
|
const imgContainer = document.createElement('div');
|
||||||
imgContainer.className =
|
imgContainer.className =
|
||||||
@@ -116,7 +115,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 = i.toString();
|
pageNumberSpan.textContent = pageNumber.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';
|
||||||
@@ -141,13 +140,38 @@ 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
initializePageGridSortable();
|
attachEventListeners(wrapper);
|
||||||
createIcons({ icons });
|
|
||||||
hideLoader();
|
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();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering thumbnails:', error);
|
||||||
|
showAlert('Error', 'Failed to render page previews');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processAndSave() {
|
export async function processAndSave() {
|
||||||
@@ -156,11 +180,28 @@ 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).map((el) =>
|
const finalIndices = Array.from(finalPageElements)
|
||||||
parseInt((el as HTMLElement).dataset.originalPageIndex)
|
.map((el) => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10))
|
||||||
);
|
.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));
|
||||||
|
|
||||||
@@ -170,8 +211,8 @@ export async function processAndSave() {
|
|||||||
'organized.pdf'
|
'organized.pdf'
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error('Save error:', e);
|
||||||
showAlert('Error', 'Failed to save the new PDF.');
|
showAlert('Error', 'Failed to save the new PDF. Check console for details.');
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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');
|
||||||
@@ -66,6 +67,30 @@ 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;
|
||||||
|
|
||||||
@@ -76,28 +101,59 @@ 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');
|
|
||||||
document.getElementById('process-btn')?.setAttribute('disabled', 'true');
|
const fileControls = document.getElementById('file-controls');
|
||||||
|
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);
|
||||||
showStatus(`Error: ${errorMessage}`, 'error');
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,13 +169,12 @@ 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 ${
|
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
|
||||||
type === 'success'
|
? 'bg-green-900 text-green-200'
|
||||||
? 'bg-green-900 text-green-200'
|
: type === 'error'
|
||||||
: type === 'error'
|
? 'bg-red-900 text-red-200'
|
||||||
? 'bg-red-900 text-red-200'
|
: 'bg-blue-900 text-blue-200'
|
||||||
: 'bg-blue-900 text-blue-200'
|
}`;
|
||||||
}`;
|
|
||||||
statusMessage.classList.remove('hidden');
|
statusMessage.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,11 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile, getPDFDocument } 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) {
|
||||||
@@ -12,8 +16,7 @@ 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();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||||
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);
|
||||||
@@ -22,7 +25,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 }).promise;
|
await page.render({ canvasContext: ctx, viewport, canvas }).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,5 +1,6 @@
|
|||||||
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));
|
||||||
|
|
||||||
@@ -17,10 +18,10 @@ function showStatus(
|
|||||||
) {
|
) {
|
||||||
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'
|
||||||
: 'bg-blue-900 text-blue-200'
|
: 'bg-blue-900 text-blue-200'
|
||||||
}`
|
}`
|
||||||
statusMessage.classList.remove('hidden')
|
statusMessage.classList.remove('hidden')
|
||||||
}
|
}
|
||||||
@@ -155,4 +156,4 @@ convertBtn.addEventListener('click', convertJSONsToPDF)
|
|||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
showStatus('Select JSON files to get started', 'info')
|
showStatus('Select JSON files to get started', 'info')
|
||||||
|
initializeGlobalShortcuts()
|
||||||
|
|||||||
+72
-55
@@ -1,15 +1,14 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } 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 { 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(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
|
||||||
import.meta.url
|
|
||||||
).toString();
|
|
||||||
|
|
||||||
const mergeState = {
|
const mergeState = {
|
||||||
pdfDocs: {},
|
pdfDocs: {},
|
||||||
@@ -129,71 +128,89 @@ async function renderPageMergeThumbnails() {
|
|||||||
mergeState.isRendering = true;
|
mergeState.isRendering = true;
|
||||||
container.textContent = '';
|
container.textContent = '';
|
||||||
|
|
||||||
let currentPageNumber = 0;
|
// Cleanup any previous lazy loading observers
|
||||||
|
cleanupLazyRendering();
|
||||||
|
|
||||||
let totalPages = state.files.reduce((sum, file) => {
|
let totalPages = state.files.reduce((sum, file) => {
|
||||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||||
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const thumbnailsHTML = [];
|
let currentPageNumber = 0;
|
||||||
|
|
||||||
|
// Function to create wrapper element for each page
|
||||||
|
const createWrapper = (canvas: HTMLCanvasElement, pageNumber: number, fileName?: string) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
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';
|
||||||
|
wrapper.dataset.fileName = fileName || '';
|
||||||
|
wrapper.dataset.pageIndex = (pageNumber - 1).toString();
|
||||||
|
|
||||||
|
const imgContainer = document.createElement('div');
|
||||||
|
imgContainer.className = 'relative';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = canvas.toDataURL();
|
||||||
|
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
||||||
|
|
||||||
|
const pageNumDiv = document.createElement('div');
|
||||||
|
pageNumDiv.className =
|
||||||
|
'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();
|
||||||
|
|
||||||
|
imgContainer.append(img, pageNumDiv);
|
||||||
|
|
||||||
|
const fileNamePara = document.createElement('p');
|
||||||
|
fileNamePara.className =
|
||||||
|
'text-xs text-gray-400 truncate w-full text-center';
|
||||||
|
const fullTitle = fileName ? `${fileName} (page ${pageNumber})` : `Page ${pageNumber}`;
|
||||||
|
fileNamePara.title = fullTitle;
|
||||||
|
fileNamePara.textContent = fileName
|
||||||
|
? `${fileName.substring(0, 10)}... (p${pageNumber})`
|
||||||
|
: `Page ${pageNumber}`;
|
||||||
|
|
||||||
|
wrapper.append(imgContainer, fileNamePara);
|
||||||
|
return wrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render pages from all files progressively
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||||
if (!pdfDoc) continue;
|
if (!pdfDoc) continue;
|
||||||
|
|
||||||
const pdfData = await pdfDoc.save();
|
const pdfData = await pdfDoc.save();
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise;
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
// Create a wrapper function that includes the file name
|
||||||
currentPageNumber++;
|
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
||||||
showLoader(
|
return createWrapper(canvas, pageNumber, file.name);
|
||||||
`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;
|
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
// Render pages progressively with lazy loading
|
||||||
wrapper.className =
|
await renderPagesProgressively(
|
||||||
'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';
|
pdfjsDoc,
|
||||||
wrapper.dataset.fileName = file.name;
|
container,
|
||||||
wrapper.dataset.pageIndex = (i - 1).toString();
|
createWrapperWithFileName,
|
||||||
|
{
|
||||||
|
batchSize: 6,
|
||||||
|
useLazyLoading: true,
|
||||||
|
lazyLoadMargin: '300px',
|
||||||
|
onProgress: (current, total) => {
|
||||||
|
currentPageNumber++;
|
||||||
|
showLoader(
|
||||||
|
`Rendering page previews...`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onBatchComplete: () => {
|
||||||
|
createIcons({ icons });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const imgContainer = document.createElement('div');
|
// TODO@ALAM - DON'T destroy the PDF.js document here - lazy loading still needs it!
|
||||||
imgContainer.className = 'relative';
|
// It will be garbage collected automatically when no longer referenced
|
||||||
|
// pdfjsDoc.destroy(); // REMOVED
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = canvas.toDataURL();
|
|
||||||
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
|
||||||
|
|
||||||
const pageNumDiv = document.createElement('div');
|
|
||||||
pageNumDiv.className =
|
|
||||||
'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
|
|
||||||
pageNumDiv.textContent = i.toString();
|
|
||||||
|
|
||||||
imgContainer.append(img, pageNumDiv);
|
|
||||||
|
|
||||||
const fileNamePara = document.createElement('p');
|
|
||||||
fileNamePara.className =
|
|
||||||
'text-xs text-gray-400 truncate w-full text-center';
|
|
||||||
const fullTitle = `${file.name} (page ${i})`;
|
|
||||||
fileNamePara.title = fullTitle;
|
|
||||||
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
|
||||||
|
|
||||||
wrapper.append(imgContainer, fileNamePara);
|
|
||||||
container.appendChild(wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
pdfjsDoc.destroy();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeState.cachedThumbnails = true;
|
mergeState.cachedThumbnails = true;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
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 } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } 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;
|
||||||
|
|
||||||
@@ -117,8 +121,7 @@ async function runOCR() {
|
|||||||
tessedit_char_whitelist: whitelist,
|
tessedit_char_whitelist: whitelist,
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdf = await getPDFDocument(
|
||||||
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();
|
||||||
@@ -136,7 +139,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 }).promise;
|
await page.render({ canvasContext: context, viewport, canvas }).promise;
|
||||||
|
|
||||||
if (binarize) {
|
if (binarize) {
|
||||||
binarizeCanvas(context);
|
binarizeCanvas(context);
|
||||||
|
|||||||
@@ -1,8 +1,120 @@
|
|||||||
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').
|
||||||
@@ -16,31 +128,89 @@ 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');
|
||||||
|
|
||||||
// Create and append each cell safely using textContent
|
// Page number
|
||||||
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;
|
||||||
|
|
||||||
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
|
// Aspect Ratio
|
||||||
|
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.
|
||||||
@@ -53,28 +223,36 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+393
-117
@@ -1,12 +1,11 @@
|
|||||||
// @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 } from '../utils/helpers';
|
import { downloadFile, getPDFDocument } 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',
|
||||||
@@ -14,13 +13,19 @@ 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;
|
canvas: HTMLCanvasElement | null;
|
||||||
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[] = [];
|
||||||
@@ -90,6 +95,8 @@ 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');
|
||||||
@@ -99,7 +106,27 @@ 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... ${current} of ${total}`;
|
text.textContent = `Rendering pages...`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
@@ -107,18 +134,28 @@ function hideLoading() {
|
|||||||
if (loader) loader.classList.add('hidden');
|
if (loader) loader.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
console.log('PDF Multi Tool: DOMContentLoaded');
|
||||||
|
initializeTool();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('PDF Multi Tool: DOMContentLoaded already fired, initializing immediately');
|
||||||
initializeTool();
|
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;
|
||||||
@@ -156,19 +193,36 @@ function initializeTool() {
|
|||||||
});
|
});
|
||||||
document.getElementById('bulk-download-btn')?.addEventListener('click', () => {
|
document.getElementById('bulk-download-btn')?.addEventListener('click', () => {
|
||||||
if (isRendering) return;
|
if (isRendering) return;
|
||||||
bulkDownload();
|
if (selectedPages.size === 0) {
|
||||||
|
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;
|
||||||
downloadAll();
|
if (allPages.length === 0) {
|
||||||
|
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;
|
||||||
@@ -240,21 +294,25 @@ 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();
|
||||||
renderCancelled = false;
|
cleanupLazyRendering();
|
||||||
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');
|
||||||
}
|
}
|
||||||
@@ -276,43 +334,80 @@ 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;
|
|
||||||
let currentPage = 0;
|
// Cleanup previous observers
|
||||||
|
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);
|
||||||
pdfDocs.push(pdfDoc);
|
currentPdfDocs.push(pdfDoc);
|
||||||
totalPages += pdfDoc.getPageCount();
|
const pdfIndex = currentPdfDocs.length - 1;
|
||||||
|
|
||||||
|
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 });
|
||||||
@@ -320,9 +415,13 @@ 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,79 +429,63 @@ function getCacheKey(pdfIndex: number, pageIndex: number): string {
|
|||||||
return `${pdfIndex}-${pageIndex}`;
|
return `${pdfIndex}-${pageIndex}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderPage(pdfDoc: PDFLibDocument, pageIndex: number, pdfIndex: number) {
|
// Wrapper for compatibility with updatePageDisplay
|
||||||
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';
|
preview.className = 'bg-white rounded mb-2 overflow-hidden w-full flex items-center justify-center relative h-36 sm:h-64';
|
||||||
preview.style.minHeight = '160px';
|
|
||||||
preview.style.height = '250px';
|
|
||||||
|
|
||||||
const previewCanvas = pageData.canvas;
|
if (canvas) {
|
||||||
previewCanvas.className = 'max-w-full max-h-full object-contain';
|
const previewCanvas = canvas;
|
||||||
|
previewCanvas.className = 'max-w-full max-h-full object-contain';
|
||||||
|
|
||||||
// Apply visual rotation using CSS transform
|
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
||||||
previewCanvas.style.transform = `rotate(${pageData.visualRotation}deg)`;
|
previewCanvas.style.transition = 'transform 0.2s ease';
|
||||||
previewCanvas.style.transition = 'transform 0.2s ease';
|
preview.appendChild(previewCanvas);
|
||||||
|
} else {
|
||||||
preview.appendChild(previewCanvas);
|
// 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');
|
||||||
@@ -491,9 +574,16 @@ function createPageCard(pageData: PageData, index: number) {
|
|||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
createIcons({ icons });
|
// Check for split marker
|
||||||
|
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() {
|
||||||
@@ -512,6 +602,9 @@ 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!;
|
||||||
@@ -600,6 +693,7 @@ function duplicatePage(index: number) {
|
|||||||
|
|
||||||
const newPageData: PageData = {
|
const newPageData: PageData = {
|
||||||
...originalPageData,
|
...originalPageData,
|
||||||
|
id: generateId(), // New ID for the duplicate
|
||||||
canvas: newCanvas,
|
canvas: newCanvas,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -643,18 +737,65 @@ 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++) {
|
||||||
// Use the existing renderPage function, which adds to allPages
|
newPages.push({
|
||||||
await renderPage(pdfDoc, i, currentPdfDocs.length - 1);
|
id: generateId(),
|
||||||
// Move the newly added page data to the temporary array
|
pdfIndex,
|
||||||
newPages.push(allPages.pop()!);
|
pageIndex: i,
|
||||||
|
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');
|
||||||
@@ -695,6 +836,7 @@ function addBlankPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const blankPageData: PageData = {
|
const blankPageData: PageData = {
|
||||||
|
id: generateId(),
|
||||||
pdfIndex: -1,
|
pdfIndex: -1,
|
||||||
pageIndex: -1,
|
pageIndex: -1,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
@@ -702,6 +844,7 @@ 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);
|
||||||
@@ -716,11 +859,26 @@ function bulkRotate(delta: number) {
|
|||||||
|
|
||||||
selectedPages.forEach(index => {
|
selectedPages.forEach(index => {
|
||||||
const pageData = allPages[index];
|
const pageData = allPages[index];
|
||||||
pageData.visualRotation = (pageData.visualRotation + delta + 360) % 360;
|
if (pageData) {
|
||||||
pageData.rotation = (pageData.rotation + delta + 360) % 360;
|
// Update state
|
||||||
|
pageData.visualRotation = (pageData.visualRotation + 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updatePageDisplay();
|
// TODO@ALAM - Do NOT call updatePageDisplay() as it destroys lazy loading observers
|
||||||
}
|
}
|
||||||
|
|
||||||
function bulkDelete() {
|
function bulkDelete() {
|
||||||
@@ -769,14 +927,6 @@ 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) {
|
||||||
@@ -826,6 +976,10 @@ 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);
|
||||||
@@ -840,7 +994,7 @@ async function downloadSplitPdfs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await newPdf.save();
|
const pdfBytes = await newPdf.save();
|
||||||
zip.file(`document-${segIndex + 1}.pdf`, pdfBytes);
|
zip.file(`document - ${segIndex + 1}.pdf`, pdfBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate and download ZIP
|
// Generate and download ZIP
|
||||||
@@ -851,6 +1005,8 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,6 +1016,10 @@ 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]);
|
||||||
@@ -878,6 +1038,7 @@ 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');
|
||||||
@@ -888,15 +1049,130 @@ function updatePageDisplay() {
|
|||||||
const pagesContainer = document.getElementById('pages-container');
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
if (!pagesContainer) return;
|
if (!pagesContainer) return;
|
||||||
|
|
||||||
pagesContainer.innerHTML = '';
|
// 1. Create a map of existing elements by ID
|
||||||
allPages.forEach((pageData, index) => {
|
const existingElements = new Map<string, HTMLElement>();
|
||||||
createPageCard(pageData, index);
|
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) => {
|
||||||
|
let card = existingElements.get(pageData.id);
|
||||||
|
|
||||||
|
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() {
|
||||||
updatePageDisplay();
|
const pagesContainer = document.getElementById('pages-container');
|
||||||
|
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); };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } 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).
|
||||||
@@ -53,8 +56,7 @@ 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 {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
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();
|
||||||
@@ -69,7 +71,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 }).promise;
|
await page.render({ canvasContext: context, viewport: viewport, canvas }).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);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile, getPDFDocument } 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';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||||
|
|
||||||
export async function pdfToGreyscale() {
|
export async function pdfToGreyscale() {
|
||||||
if (!state.pdfDoc) {
|
if (!state.pdfDoc) {
|
||||||
showAlert('Error', 'PDF not loaded.');
|
showAlert('Error', 'PDF not loaded.');
|
||||||
@@ -13,8 +15,7 @@ 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();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
|
||||||
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);
|
||||||
@@ -24,7 +25,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 }).promise;
|
await page.render({ canvasContext: ctx, viewport, canvas }).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,13 +1,16 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } 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 {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
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();
|
||||||
@@ -23,7 +26,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 }).promise;
|
await page.render({ canvasContext: context, viewport: viewport, canvas }).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)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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));
|
||||||
|
|
||||||
@@ -17,10 +18,10 @@ function showStatus(
|
|||||||
) {
|
) {
|
||||||
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'
|
||||||
: 'bg-blue-900 text-blue-200'
|
: 'bg-blue-900 text-blue-200'
|
||||||
}`
|
}`
|
||||||
statusMessage.classList.remove('hidden')
|
statusMessage.classList.remove('hidden')
|
||||||
}
|
}
|
||||||
@@ -149,6 +150,5 @@ 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()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
export async function pdfToMarkdown() {
|
export async function pdfToMarkdown() {
|
||||||
@@ -7,8 +7,7 @@ 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);
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdf = await getPDFDocument({ data: arrayBuffer }).promise;
|
||||||
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++) {
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } 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 {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
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();
|
||||||
@@ -22,7 +25,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 }).promise;
|
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
|
||||||
const blob = await new Promise((resolve) =>
|
const blob = await new Promise((resolve) =>
|
||||||
canvas.toBlob(resolve, 'image/png')
|
canvas.toBlob(resolve, 'image/png')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } 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 pdfjsLib.getDocument(
|
const pdf = await getPDFDocument(
|
||||||
await readFileAsArrayBuffer(state.files[0])
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
).promise;
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } 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 {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
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();
|
||||||
@@ -18,7 +21,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 }).promise;
|
await page.render({ canvasContext: context, viewport: viewport, canvas }).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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, parsePageRanges } from '../utils/helpers.js';
|
import { downloadFile, parsePageRanges, getPDFDocument } 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: {},
|
||||||
@@ -121,7 +123,7 @@ export async function setupPosterizeTool() {
|
|||||||
.getPageCount()
|
.getPageCount()
|
||||||
.toString();
|
.toString();
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes })
|
posterizeState.pdfJsDoc = await getPDFDocument({ data: pdfBytes })
|
||||||
.promise;
|
.promise;
|
||||||
posterizeState.pageSnapshots = {};
|
posterizeState.pageSnapshots = {};
|
||||||
posterizeState.currentPage = 1;
|
posterizeState.currentPage = 1;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile, getPDFDocument } 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) {
|
||||||
@@ -38,7 +40,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 pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
const pdf = await getPDFDocument({ data: pdfBytes }).promise;
|
||||||
|
|
||||||
analysisCache = [];
|
analysisCache = [];
|
||||||
const promises = [];
|
const promises = [];
|
||||||
|
|||||||
+11
-7
@@ -1,6 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile, resetAndReloadTool } 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';
|
||||||
|
|
||||||
@@ -8,12 +9,11 @@ export async function rotate() {
|
|||||||
showLoader('Applying rotations...');
|
showLoader('Applying rotations...');
|
||||||
try {
|
try {
|
||||||
const pages = state.pdfDoc.getPages();
|
const pages = state.pdfDoc.getPages();
|
||||||
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
const rotationStateArray = getRotationState();
|
||||||
// @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(item.dataset.pageIndex);
|
// Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
rotationStateArray.forEach((rotation, pageIndex) => {
|
||||||
const rotation = parseInt(item.dataset.rotation || '0');
|
if (rotation !== 0 && pages[pageIndex]) {
|
||||||
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,6 +24,10 @@ 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.');
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
-22
@@ -1,3 +1,4 @@
|
|||||||
|
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';
|
||||||
@@ -40,9 +41,21 @@ 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);
|
||||||
|
|
||||||
const viewerBase = '/pdfjs-viewer/viewer.html';
|
try {
|
||||||
|
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 = `${viewerBase}?${query.toString()}`;
|
iframe.src = `${viewerUrl.toString()}?${query.toString()}`;
|
||||||
|
|
||||||
iframe.onload = () => {
|
iframe.onload = () => {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -52,23 +65,27 @@ 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;
|
||||||
const editorModeButtons = doc.getElementById('editorModeButtons');
|
eventBus?._on('annotationeditoruimanager', () => {
|
||||||
editorModeButtons?.classList.remove('hidden');
|
const editorModeButtons = doc.getElementById('editorModeButtons');
|
||||||
|
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);
|
||||||
@@ -99,11 +116,41 @@ export async function applyAndSaveSignatures() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegate to the built-in download behavior of the base viewer.
|
|
||||||
const app = viewerWindow.PDFViewerApplication;
|
const app = viewerWindow.PDFViewerApplication;
|
||||||
app.eventBus?.dispatch('download', { source: app });
|
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.
|
||||||
|
app.eventBus?.dispatch('download', { source: app });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to trigger download in base PDF.js viewer:', error);
|
console.error('Failed to export the signed PDF:', 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.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-19
@@ -1,6 +1,11 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { createIcons, icons } from 'lucide';
|
||||||
|
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';
|
||||||
@@ -17,35 +22,29 @@ 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();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
const pdf = await getPDFDocument({ data: pdfData }).promise;
|
||||||
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 = i - 1;
|
wrapper.dataset.pageIndex = pageNumber - 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 ${i}`;
|
p.textContent = `Page ${pageNumber}`;
|
||||||
wrapper.append(img, p);
|
wrapper.append(img, p);
|
||||||
|
|
||||||
const handleSelection = (e: any) => {
|
const handleSelection = (e: any) => {
|
||||||
@@ -69,12 +68,31 @@ 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.');
|
||||||
// 4. ADDED: Reset the flag on error so the user can try again.
|
// Reset the flag on error so the user can try again.
|
||||||
visualSelectorRendered = false;
|
visualSelectorRendered = false;
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
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');
|
||||||
|
|
||||||
@@ -54,10 +56,10 @@ function showStatus(
|
|||||||
) {
|
) {
|
||||||
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'
|
||||||
: 'bg-blue-900 text-blue-200'
|
: 'bg-blue-900 text-blue-200'
|
||||||
}`;
|
}`;
|
||||||
statusMessage.classList.remove('hidden');
|
statusMessage.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
@@ -202,3 +204,5 @@ if (backToToolsBtn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateBtn.addEventListener('click', generateTableOfContents);
|
generateBtn.addEventListener('click', generateTableOfContents);
|
||||||
|
|
||||||
|
initializeGlobalShortcuts();
|
||||||
|
|||||||
@@ -154,6 +154,11 @@ 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() {
|
||||||
|
|||||||
+532
-9
@@ -1,17 +1,18 @@
|
|||||||
import { categories } from './config/tools.js';
|
import { categories } from './config/tools.js';
|
||||||
import { dom, switchView, hideAlert } from './ui.js';
|
import { dom, switchView, hideAlert, showLoader, hideLoader, showAlert } 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 { formatStars } from './utils/helpers.js';
|
import { formatShortcutDisplay, formatStars } from './utils/helpers.js';
|
||||||
|
import { APP_VERSION, injectVersion } from '../version.js';
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||||
'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 = () => {
|
||||||
@@ -28,9 +29,11 @@ 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">
|
<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">
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
||||||
<span class="text-white font-bold text-xl ml-2">BentoPDF</span>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +97,7 @@ const init = () => {
|
|||||||
© 2025 BentoPDF. All rights reserved.
|
© 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">1.6.2</span>
|
Version <span id="app-version-simple">${APP_VERSION}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -131,6 +134,16 @@ 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) => {
|
||||||
@@ -266,9 +279,18 @@ 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 githubStarsElement = document.getElementById('github-stars');
|
||||||
if (githubStarsElement && !__SIMPLE_MODE__) {
|
if (githubStarsElement && !__SIMPLE_MODE__) {
|
||||||
fetch('https://api.github.com/repos/alam00000/bentopdf')
|
fetch('https://api.github.com/repos/alam00000/bentopdf')
|
||||||
@@ -282,6 +304,507 @@ const init = () => {
|
|||||||
githubStarsElement.textContent = '-';
|
githubStarsElement.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'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|||||||
+401
-259
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+93
-5
@@ -1,6 +1,9 @@
|
|||||||
import createModule from '@neslinesli93/qpdf-wasm';
|
import createModule from '@neslinesli93/qpdf-wasm';
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
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 },
|
||||||
@@ -45,16 +48,17 @@ export function convertPoints(points: any, unit: any) {
|
|||||||
return result.toFixed(2);
|
return result.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const hexToRgb = (hex: any) => {
|
// Convert hex color to RGB
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
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)
|
||||||
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 }; // Default to black
|
: { r: 0, g: 0, b: 0 }
|
||||||
};
|
}
|
||||||
|
|
||||||
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';
|
||||||
@@ -194,3 +198,87 @@ 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/',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ShortcutsManager } from '../logic/shortcuts.js';
|
||||||
|
|
||||||
|
export function initializeGlobalShortcuts() {
|
||||||
|
ShortcutsManager.init();
|
||||||
|
|
||||||
|
console.log('Global shortcuts initialized');
|
||||||
|
}
|
||||||
+16
-80
@@ -59,7 +59,8 @@
|
|||||||
</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 class="bg-gray-800 rounded-xl shadow-xl p-6 max-w-5xl w-full text-gray-200 border border-gray-700">
|
<div id="tool-uploader"
|
||||||
|
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>
|
||||||
@@ -93,99 +94,34 @@
|
|||||||
<h2 class="text-lg font-semibold text-white">Stamp Editor</h2>
|
<h2 class="text-lg font-semibold text-white">Stamp Editor</h2>
|
||||||
<p class="text-xs text-gray-400">Use the toolbar's image stamp tool to place stamps.</p>
|
<p class="text-xs text-gray-400">Use the toolbar's image stamp tool to place stamps.</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="stamp-viewer-container" class="h-[70vh] bg-black/70 flex items-stretch justify-stretch">
|
<div id="stamp-viewer-container" class="h-[70vh] bg-black/70 flex items-stretch justify-stretch">
|
||||||
<!-- iframe injected by add-stamps.ts -->
|
<!-- iframe injected by add-stamps.ts -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="save-stamped-btn"
|
<button id="save-stamped-btn" class="btn-gradient w-full mt-6 flex items-center justify-center gap-2">
|
||||||
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 bg-[#111827]">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<div class="mb-8 md:mb-0">
|
<img src="../../public/images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8" />
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||||
<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">
|
|
||||||
© 2025 BentoPDF. All rights reserved.
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
|
||||||
Version <span id="app-version">1.6.2</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="../../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/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.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>
|
|
||||||
</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>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs mt-1">
|
||||||
|
Version <span id="app-version"></span>
|
||||||
|
</p>
|
||||||
</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="../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>
|
||||||
|
|||||||
+15
-77
@@ -61,14 +61,16 @@
|
|||||||
</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 class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-md w-full text-gray-200 border border-gray-700">
|
<div id="tool-uploader"
|
||||||
|
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">
|
||||||
Upload a PDF to begin editing bookmarks
|
Add, edit, import, delete and extract PDF bookmarks.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Drop Zone for Main PDF Upload -->
|
<!-- Drop Zone for Main PDF Upload -->
|
||||||
@@ -410,85 +412,21 @@
|
|||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<div class="mb-8 md:mb-0">
|
<img src="../../public/images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8" />
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||||
<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">
|
|
||||||
© 2025 BentoPDF. All rights reserved.
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
|
||||||
Version <span id="app-version">1.6.2</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="./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/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
|
||||||
</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>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs mt-1">
|
||||||
|
Version <span id="app-version"></span>
|
||||||
|
</p>
|
||||||
</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="../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>
|
||||||
|
|||||||
@@ -0,0 +1,337 @@
|
|||||||
|
<!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="flex items-center gap-3 mb-2">
|
||||||
|
<img src="../../public/images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8" />
|
||||||
|
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs mt-1">
|
||||||
|
Version <span id="app-version"></span>
|
||||||
|
</p>
|
||||||
|
</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="../version.ts"></script>
|
||||||
|
<script type="module" src="../js/logic/form-creator.ts"></script>
|
||||||
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
+14
-77
@@ -59,7 +59,8 @@
|
|||||||
</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 class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
<div id="tool-uploader"
|
||||||
|
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>
|
||||||
@@ -104,88 +105,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<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="flex items-center gap-3 mb-2">
|
||||||
<div class="mb-8 md:mb-0">
|
<img src="../../public/images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8" />
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||||
<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">
|
|
||||||
© 2025 BentoPDF. All rights reserved.
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
|
||||||
Version <span id="app-version">1.6.2</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="../../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/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
|
||||||
</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>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs mt-1">
|
||||||
|
Version <span id="app-version"></span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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="../version.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" class="overflow-hidden">
|
||||||
|
|
||||||
<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-screen overflow-hidden">
|
<body class="antialiased bg-gray-900 h-[100dvh] overflow-hidden flex flex-col">
|
||||||
|
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<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 flex-none 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 h-[calc(100vh-4rem)]">
|
<div class="flex flex-col flex-1 overflow-hidden relative">
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<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 class="bg-gray-800 border-b border-gray-700 p-2 sm:p-3 toolbar-container flex-none 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 class="flex-1 overflow-auto p-3 sm:p-4 sm:pt-20 sm:pb-8 md:pt-4 md:pb-4">
|
<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 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()">
|
||||||
|
|||||||
+14
-77
@@ -59,7 +59,8 @@
|
|||||||
</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 class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
<div id="tool-uploader"
|
||||||
|
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>
|
||||||
@@ -98,88 +99,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<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="flex items-center gap-3 mb-2">
|
||||||
<div class="mb-8 md:mb-0">
|
<img src="../../public/images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8" />
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||||
<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">
|
|
||||||
© 2025 BentoPDF. All rights reserved.
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
|
||||||
Version <span id="app-version">1.6.2</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="../../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/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
|
||||||
</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>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs mt-1">
|
||||||
|
Version <span id="app-version"></span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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="../version.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,8 @@
|
|||||||
</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 class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
<div id="tool-uploader"
|
||||||
|
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>
|
||||||
@@ -170,84 +171,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<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="flex items-center gap-3 mb-2">
|
||||||
<div class="mb-8 md:mb-0">
|
<img src="../../public/images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8" />
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||||
<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">
|
|
||||||
© 2025 BentoPDF. All rights reserved.
|
|
||||||
</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="../../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/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
||||||
<path
|
|
||||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
|
||||||
</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>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs mt-1">
|
||||||
|
Version <span id="app-version"></span>
|
||||||
|
</p>
|
||||||
</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/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>
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+259
-293
@@ -1,309 +1,275 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Terms and Conditions - 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 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 -->
|
<head>
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<meta charset="UTF-8" />
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<title>Terms and Conditions - Bentopdf</title>
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<link rel="icon" type="image/png" href="images/favicon.svg" />
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
</div>
|
</head>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<div class="md:hidden flex items-center">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<button
|
<div class="container mx-auto px-4">
|
||||||
id="mobile-menu-button"
|
<div class="flex justify-between items-center h-16">
|
||||||
type="button"
|
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||||
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"
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||||
aria-controls="mobile-menu"
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
aria-expanded="false"
|
<a href="index.html">BentoPDF</a>
|
||||||
>
|
</span>
|
||||||
<span class="sr-only">Open main menu</span>
|
</div>
|
||||||
<!-- Hamburger Icon -->
|
|
||||||
<svg
|
<!-- Desktop Navigation -->
|
||||||
id="menu-icon"
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
class="block h-6 w-6"
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
fill="none"
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
viewBox="0 0 24 24"
|
<a href="./licensing.html" class="nav-link">Licensing</a>
|
||||||
stroke="currentColor"
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
aria-hidden="true"
|
</div>
|
||||||
>
|
|
||||||
<path
|
<!-- Mobile Hamburger Button -->
|
||||||
stroke-linecap="round"
|
<div class="md:hidden flex items-center">
|
||||||
stroke-linejoin="round"
|
<button id="mobile-menu-button" type="button"
|
||||||
stroke-width="2"
|
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"
|
||||||
d="M4 6h16M4 12h16M4 18h16"
|
aria-controls="mobile-menu" aria-expanded="false">
|
||||||
/>
|
<span class="sr-only">Open main menu</span>
|
||||||
</svg>
|
<!-- Hamburger Icon -->
|
||||||
<!-- Close 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="close-icon"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
class="hidden h-6 w-6"
|
</svg>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<!-- Close Icon -->
|
||||||
fill="none"
|
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
stroke="currentColor"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
aria-hidden="true"
|
</svg>
|
||||||
>
|
</button>
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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="index.html#tools-header" class="mobile-nav-link"
|
|
||||||
>All Tools</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div id="app" class="container mx-auto p-4 md:p-8">
|
|
||||||
<section class="max-w-4xl mx-auto py-12">
|
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-center text-white mb-4">
|
|
||||||
Terms and Conditions
|
|
||||||
</h1>
|
|
||||||
<p class="text-center text-gray-500">
|
|
||||||
Last Updated: September 14, 2025
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="legal-content mt-12">
|
|
||||||
<h2>1. Acceptance of Terms</h2>
|
|
||||||
<p>
|
|
||||||
By accessing and using Bentopdf (the "Service"), you accept and
|
|
||||||
agree to be bound by the terms and provision of this agreement. If
|
|
||||||
you do not agree to abide by these terms, please do not use this
|
|
||||||
service.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>2. Description of Service</h2>
|
|
||||||
<p>
|
|
||||||
Bentopdf provides a suite of client-side tools for processing and
|
|
||||||
manipulating Portable Document Format (PDF) files. All operations
|
|
||||||
performed by the Service are executed locally within your web
|
|
||||||
browser.
|
|
||||||
<strong
|
|
||||||
>No files or data are uploaded to or stored on our
|
|
||||||
servers.</strong
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>3. User Conduct and Responsibilities</h2>
|
|
||||||
<p>
|
|
||||||
You are solely responsible for the content of the files you process
|
|
||||||
with our Service. You agree not to use the Service for any unlawful
|
|
||||||
purpose, including but not limited to:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Processing any material that infringes on the copyright,
|
|
||||||
trademark, or intellectual property rights of others.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Processing any material that is defamatory, libelous, obscene, or
|
|
||||||
otherwise illegal.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Attempting to reverse-engineer, decompile, or otherwise disrupt
|
|
||||||
the functionality of the Service.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>4. Disclaimer of Warranties</h2>
|
|
||||||
<p>
|
|
||||||
The Service is provided "as is" and "as available" without any
|
|
||||||
warranties of any kind, express or implied. We do not warrant that
|
|
||||||
the service will be error-free, uninterrupted, or that the results
|
|
||||||
obtained from using the tools will be accurate, complete, or
|
|
||||||
reliable. You acknowledge that you use the Service at your own risk.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Specifically, we do not guarantee that file conversions,
|
|
||||||
compressions, or modifications will be perfect. Data loss or
|
|
||||||
corruption, while unlikely, is a possibility. It is your
|
|
||||||
responsibility to maintain backups of your original files.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>5. Limitation of Liability</h2>
|
|
||||||
<p>
|
|
||||||
To the fullest extent permitted by applicable law, in no event shall
|
|
||||||
Bentopdf, its developers, or its affiliates be liable for any
|
|
||||||
indirect, incidental, special, consequential, or punitive damages,
|
|
||||||
including but not limited to, loss of profits, data, use, goodwill,
|
|
||||||
or other intangible losses, resulting from:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Your access to or use of or inability to access or use the
|
|
||||||
Service.
|
|
||||||
</li>
|
|
||||||
<li>Any conduct or content of any third party on the Service.</li>
|
|
||||||
<li>Any content obtained from the Service.</li>
|
|
||||||
<li>
|
|
||||||
Unauthorized access, use, or alteration of your transmissions or
|
|
||||||
content.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Our total liability to you for any and all claims arising out of
|
|
||||||
your use of this free service shall not exceed the amount of zero
|
|
||||||
dollars ($0.00).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>6. Intellectual Property</h2>
|
|
||||||
<p>
|
|
||||||
The visual interfaces, graphics, design, compilation, information,
|
|
||||||
computer code, products, software, services, and all other elements
|
|
||||||
of the Service provided by Bentopdf are protected by intellectual
|
|
||||||
property and other laws. All materials contained on the Service are
|
|
||||||
the property of Bentopdf or our third-party licensors.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>7. Governing Law</h2>
|
|
||||||
<p>
|
|
||||||
These Terms shall be governed and construed in accordance with the
|
|
||||||
laws of India, without regard to its conflict of law provisions.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>8. Changes to Terms</h2>
|
|
||||||
<p>
|
|
||||||
We reserve the right, at our sole discretion, to modify or replace
|
|
||||||
these Terms at any time. We will provide notice of changes by
|
|
||||||
updating the "Last Updated" date at the top of this page. By
|
|
||||||
continuing to access or use our Service after those revisions become
|
|
||||||
effective, you agree to be bound by the revised terms.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2>9. Contact Us</h2>
|
|
||||||
<p>
|
|
||||||
If you have any questions about these Terms, please contact us at
|
|
||||||
<a href="mailto:[email protected]">[email protected]</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div class="container mx-auto px-4">
|
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<div class="mb-8 md:mb-0">
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<a href="./licensing.html" class="mobile-nav-link">Licensing</a>
|
||||||
</div>
|
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
||||||
<p class="text-gray-400 text-sm">
|
</div>
|
||||||
© 2025 BentoPDF. All rights reserved.
|
</div>
|
||||||
</p>
|
</nav>
|
||||||
<p class="text-gray-500 text-xs mt-2">
|
<div id="app" class="container mx-auto p-4 md:p-8">
|
||||||
Version <span id="app-version">1.6.2</span>
|
<section class="max-w-4xl mx-auto py-12">
|
||||||
</p>
|
<h1 class="text-4xl md:text-5xl font-bold text-center text-white mb-4">
|
||||||
</div>
|
Terms and Conditions
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-500">
|
||||||
|
Last Updated: September 14, 2025
|
||||||
|
</p>
|
||||||
|
|
||||||
<div>
|
<div class="legal-content mt-12">
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h2>1. Acceptance of Terms</h2>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<p>
|
||||||
<li>
|
By accessing and using Bentopdf (the "Service"), you accept and
|
||||||
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
agree to be bound by the terms and provision of this agreement. If
|
||||||
</li>
|
you do not agree to abide by these terms, please do not use this
|
||||||
<li>
|
service.
|
||||||
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
</p>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<h2>2. Description of Service</h2>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<p>
|
||||||
<ul class="space-y-2 text-gray-400">
|
Bentopdf provides a suite of client-side tools for processing and
|
||||||
<li>
|
manipulating Portable Document Format (PDF) files. All operations
|
||||||
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
performed by the Service are executed locally within your web
|
||||||
</li>
|
browser.
|
||||||
<li>
|
<strong>No files or data are uploaded to or stored on our
|
||||||
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
servers.</strong>
|
||||||
</li>
|
</p>
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<h2>3. User Conduct and Responsibilities</h2>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<p>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
You are solely responsible for the content of the files you process
|
||||||
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
with our Service. You agree not to use the Service for any unlawful
|
||||||
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
purpose, including but not limited to:
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
</p>
|
||||||
<path fill-rule="evenodd"
|
<ul>
|
||||||
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"
|
<li>
|
||||||
clip-rule="evenodd" />
|
Processing any material that infringes on the copyright,
|
||||||
</svg>
|
trademark, or intellectual property rights of others.
|
||||||
</a>
|
</li>
|
||||||
<a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
|
<li>
|
||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
Processing any material that is defamatory, libelous, obscene, or
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
otherwise illegal.
|
||||||
<path
|
</li>
|
||||||
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" />
|
<li>
|
||||||
</svg>
|
Attempting to reverse-engineer, decompile, or otherwise disrupt
|
||||||
</a>
|
the functionality of the Service.
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
</li>
|
||||||
title="Instagram">
|
</ul>
|
||||||
<i data-lucide="instagram"></i>
|
|
||||||
</a>
|
<h2>4. Disclaimer of Warranties</h2>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
<p>
|
||||||
title="LinkedIn">
|
The Service is provided "as is" and "as available" without any
|
||||||
<i data-lucide="linkedin"></i>
|
warranties of any kind, express or implied. We do not warrant that
|
||||||
</a>
|
the service will be error-free, uninterrupted, or that the results
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
|
obtained from using the tools will be accurate, complete, or
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
reliable. You acknowledge that you use the Service at your own risk.
|
||||||
<path
|
</p>
|
||||||
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" />
|
<p>
|
||||||
</svg>
|
Specifically, we do not guarantee that file conversions,
|
||||||
</a>
|
compressions, or modifications will be perfect. Data loss or
|
||||||
</div>
|
corruption, while unlikely, is a possibility. It is your
|
||||||
|
responsibility to maintain backups of your original files.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>5. Limitation of Liability</h2>
|
||||||
|
<p>
|
||||||
|
To the fullest extent permitted by applicable law, in no event shall
|
||||||
|
Bentopdf, its developers, or its affiliates be liable for any
|
||||||
|
indirect, incidental, special, consequential, or punitive damages,
|
||||||
|
including but not limited to, loss of profits, data, use, goodwill,
|
||||||
|
or other intangible losses, resulting from:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Your access to or use of or inability to access or use the
|
||||||
|
Service.
|
||||||
|
</li>
|
||||||
|
<li>Any conduct or content of any third party on the Service.</li>
|
||||||
|
<li>Any content obtained from the Service.</li>
|
||||||
|
<li>
|
||||||
|
Unauthorized access, use, or alteration of your transmissions or
|
||||||
|
content.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Our total liability to you for any and all claims arising out of
|
||||||
|
your use of this free service shall not exceed the amount of zero
|
||||||
|
dollars ($0.00).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>6. Intellectual Property</h2>
|
||||||
|
<p>
|
||||||
|
The visual interfaces, graphics, design, compilation, information,
|
||||||
|
computer code, products, software, services, and all other elements
|
||||||
|
of the Service provided by Bentopdf are protected by intellectual
|
||||||
|
property and other laws. All materials contained on the Service are
|
||||||
|
the property of Bentopdf or our third-party licensors.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>7. Governing Law</h2>
|
||||||
|
<p>
|
||||||
|
These Terms shall be governed and construed in accordance with the
|
||||||
|
laws of India, without regard to its conflict of law provisions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>8. Changes to Terms</h2>
|
||||||
|
<p>
|
||||||
|
We reserve the right, at our sole discretion, to modify or replace
|
||||||
|
these Terms at any time. We will provide notice of changes by
|
||||||
|
updating the "Last Updated" date at the top of this page. By
|
||||||
|
continuing to access or use our Service after those revisions become
|
||||||
|
effective, you agree to be bound by the revised terms.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>9. Contact Us</h2>
|
||||||
|
<p>
|
||||||
|
If you have any questions about these Terms, please contact us at
|
||||||
|
<a href="mailto:[email protected]">[email protected]</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
© 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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
</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/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
<script type="module" src="src/js/mobileMenu.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
"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": ".",
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ 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',
|
||||||
@@ -44,6 +51,7 @@ 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'
|
||||||
@@ -51,6 +59,8 @@ 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'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user