feat: add initial project setup with core PDF tools and utilities
- Implement core PDF manipulation tools (split, merge, convert, etc.) - Add state management and UI utilities - Set up build configuration with Vite and TailwindCSS - Include essential dependencies for PDF processing - Add gitignore and basic project configuration files
This commit is contained in:
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
202
about.html
Normal file
202
about.html
Normal file
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<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>
|
||||
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="dist/styles.css">
|
||||
<link rel="icon" type="image/png" href="images/favicon.svg">
|
||||
|
||||
</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">
|
||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="images/favicon.svg" alt="Bentopdf Logo" class="h-8 w-8 mr-2">
|
||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||
</a>
|
||||
<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="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center gap-4">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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="Bentopdf 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://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
||||
<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>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
108
contact.html
Normal file
108
contact.html
Normal file
@@ -0,0 +1,108 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Contact Us - BentoPDF</title>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<link rel="stylesheet" href="dist/styles.css">
|
||||
<link rel="icon" type="image/png" href="images/favicon.svg">
|
||||
</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">
|
||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
|
||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
|
||||
<span class="text-white font-bold text-xl"> BentoPDF</span>
|
||||
</a>
|
||||
<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="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center gap-4">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||
<section id="contact-hero" class="text-center py-16 md:py-24">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">Get in Touch</h1>
|
||||
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
||||
We'd love to hear from you. Whether you have a question, feedback, or a feature request, please don't
|
||||
hesitate to reach out.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="max-w-2xl mx-auto text-center py-8">
|
||||
<p class="text-lg text-gray-400">
|
||||
You can reach us directly by email at:
|
||||
<a href="mailto:contact@bentopdf.com"
|
||||
class="text-indigo-400 underline hover:text-indigo-300">contact@bentopdf.com</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="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>
|
||||
<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://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
||||
<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>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
229
faq.html
Normal file
229
faq.html
Normal file
@@ -0,0 +1,229 @@
|
||||
<!DOCTYPE html>
|
||||
<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>
|
||||
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="dist/styles.css">
|
||||
<link rel="icon" type="image/png" href="images/favicon.svg">
|
||||
<style>
|
||||
details>summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
details>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
details>summary .icon {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
details[open]>summary .icon {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
</style>
|
||||
</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">
|
||||
<a href="index.html" 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 mr-2">
|
||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||
</a>
|
||||
<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="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center gap-4">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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">Bento PDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 2025 Bento PDF. 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://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"><i data-lucide="instagram"></i></a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"><i data-lucide="linkedin"></i></a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
||||
<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>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
579
index.html
Normal file
579
index.html
Normal file
@@ -0,0 +1,579 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BentoPDF - The Privacy First PDF Toolkit</title>
|
||||
|
||||
<!-- ANALYTICS -->
|
||||
<script async src="https://scripts.simpleanalyticscdn.com/latest.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.css" />
|
||||
|
||||
|
||||
<!-- FONTS -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400..700&family=Great+Vibes&family=Kalam:wght@300;400;700&family=Lato:ital,wght@0,400;0,700;1,400&family=Merriweather:ital,wght@0,400;0,700;1,400&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- TAILWIND -->
|
||||
<link rel="stylesheet" href="dist/styles.css">
|
||||
|
||||
<!-- LOGO -->
|
||||
<link rel="icon" type="image/png" href="./images/favicon.svg">
|
||||
|
||||
</head>
|
||||
|
||||
<body class="antialiased">
|
||||
<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>
|
||||
|
||||
<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="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center gap-2">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-menu" class="hidden md:hidden">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
|
||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||
<hr class="border-gray-700 my-2">
|
||||
<a href="#tool-grid" id="mobile-all-tools-link" class="mobile-nav-link bg-gray-700 text-white">View All
|
||||
Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||
|
||||
<section id="hero-section" class="text-center py-20">
|
||||
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
|
||||
The <span class="marker-slanted">
|
||||
PDF Toolkit
|
||||
</span> built for privacy<span
|
||||
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
|
||||
</h1>
|
||||
<p class="text-lg text-gray-400 mb-8">
|
||||
Fast, Secure and Forever Free.
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8">
|
||||
<span class="pill">No Signups</span>
|
||||
<span class="pill">Unlimited Use</span>
|
||||
<span class="pill">Works Offline</span>
|
||||
</div>
|
||||
|
||||
<a href="#tools-header"
|
||||
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5">Start
|
||||
Using - Forever Free</a>
|
||||
|
||||
</section>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<section id="features-section" class="py-20">
|
||||
<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 lg:grid-cols-3 gap-8">
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||
<h3 class="text-xl font-bold text-white">No Signup</h3>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">Start instantly, no accounts or emails.</p>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i data-lucide="shield" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||
<h3 class="text-xl font-bold text-white">No Uploads</h3>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">100% client-side, your files never leave your device.</p>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||
<h3 class="text-xl font-bold text-white">Forever Free</h3>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">All tools, no trials, no paywalls.</p>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i data-lucide="infinity" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||
<h3 class="text-xl font-bold text-white">No Limits</h3>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">Use as much as you want, no hidden caps.</p>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i data-lucide="layers" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||
<h3 class="text-xl font-bold text-white">Batch Processing</h3>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-6 rounded-lg">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
|
||||
<h3 class="text-xl font-bold text-white">Lightning Fast</h3>
|
||||
</div>
|
||||
<p class="text-gray-400 pl-14">Process PDFs instantly, without waiting or delays.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<div id="tools-header" class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-3">
|
||||
Get Started with <span class="marker-slanted ml-2"> Tools</span>
|
||||
</h2>
|
||||
<p class="text-gray-400">Click a tool to open the file uploader</p>
|
||||
</div>
|
||||
|
||||
<div id="grid-view">
|
||||
<div class="mb-8 max-w-lg mx-auto">
|
||||
<div class="relative">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<i data-lucide="search" class="w-5 h-5 text-gray-400"></i>
|
||||
</span>
|
||||
<input type="text" id="search-bar"
|
||||
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'...)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tool-grid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tool-interface"
|
||||
class="hidden w-full max-w-4xl mx-auto bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8 border border-gray-700">
|
||||
<button id="back-to-grid"
|
||||
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||
<span class="cursor-pointer"> Back to Tools </span>
|
||||
</button>
|
||||
<div id="tool-content">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div id="loader-modal"
|
||||
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-indigo-500"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div id="loader-modal"
|
||||
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
|
||||
<div class="solid-spinner"></div>
|
||||
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="alert-modal"
|
||||
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-gray-800 max-w-sm w-full p-6 rounded-lg border border-gray-700 shadow-xl text-center">
|
||||
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
|
||||
<p id="alert-message" class="text-gray-300 mb-6">This is an alert message.</p>
|
||||
<button id="alert-ok"
|
||||
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="preview-modal"
|
||||
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40 p-4">
|
||||
<div class="bg-white text-black rounded-lg shadow-xl w-full max-w-4xl h-[90vh] flex flex-col">
|
||||
<div class="p-4 border-b flex justify-between items-center">
|
||||
<h3 class="text-xl font-bold">Document Preview</h3>
|
||||
<div class="flex gap-4">
|
||||
<button id="preview-download-btn"
|
||||
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">Download
|
||||
as PDF</button>
|
||||
<button id="preview-close-btn"
|
||||
class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="preview-content" class="p-8 overflow-y-auto flex-grow">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider hide-section mb-20 mt-10"></div>
|
||||
|
||||
<!-- COMPLIANCE SECTION START -->
|
||||
<section id="security-compliance-section" class="py-20 hide-section">
|
||||
<div class="mb-8 text-center">
|
||||
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance">
|
||||
Your data never leaves your device
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<i data-lucide="shield"
|
||||
class="w-8 h-8 md:w-10 md:h-10 text-indigo-400 bg-indigo-900 rounded-lg p-1.5"></i>
|
||||
We keep
|
||||
</span>
|
||||
<br class="hidden sm:block" />
|
||||
<span
|
||||
class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold">
|
||||
your information safe
|
||||
</span>
|
||||
by following global security standards.
|
||||
</h2>
|
||||
</div>
|
||||
<div class="mb-16 text-center">
|
||||
<span class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors">
|
||||
All the processing happens locally on your device.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4">
|
||||
<!-- GDPR Compliance -->
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div
|
||||
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
|
||||
<img src="https://mkt-cf.pdffiller.com/mrk/350/images/_global/logos/security-badges/logo-gdpr.svg"
|
||||
alt="GDPR compliance" class="w-full h-full">
|
||||
</div>
|
||||
<h3 class="text-lg md:text-xl font-bold text-white mb-3">GDPR compliance</h3>
|
||||
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">Protects the personal data
|
||||
and privacy of individuals within the European Union.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div
|
||||
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
|
||||
<img src="https://mkt-cf.pdffiller.com/mrk/350/images/_global/logos/security-badges/logo-ccpa.svg"
|
||||
alt="CCPA compliance" class="w-full h-full">
|
||||
</div>
|
||||
<h3 class="text-lg md:text-xl font-bold text-white mb-3">CCPA compliance</h3>
|
||||
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">Gives California residents
|
||||
rights over how their personal information is collected, used, and shared.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div
|
||||
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
|
||||
<img src="https://mkt-cf.pdffiller.com/mrk/350/images/_global/logos/security-badges/logo-hipaa.svg"
|
||||
alt="HIPAA compliance" class="w-full h-full">
|
||||
</div>
|
||||
<h3 class="text-lg md:text-xl font-bold text-white mb-3">HIPAA compliance</h3>
|
||||
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">Sets safeguards for handling
|
||||
sensitive health information in the United States healthcare system.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- COMPLAINCE SECTION END -->
|
||||
|
||||
<div class="section-divider hide-section"></div>
|
||||
|
||||
<section id="faq-accordion" class="space-y-4 hide-section">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8">
|
||||
Frequently Asked <span class="marker-slanted">Questions</span>
|
||||
</h2>
|
||||
<!-- Existing FAQs here... -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||
<span class="text-lg font-semibold text-white">Is BentoPDF really free?</span>
|
||||
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||
</button>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Yes, absolutely. All tools on BentoPDF are 100% free to use, with no file limits, no sign-ups,
|
||||
and no
|
||||
watermarks. We believe everyone deserves access to simple, powerful PDF tools without a paywall.
|
||||
</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">Are my files secure? Where are they processed?</span>
|
||||
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||
</button>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Your files are as secure as possible because they **never leave your computer**. All processing
|
||||
happens
|
||||
directly in your web browser (client-side). We never upload your files to a server, so you
|
||||
maintain
|
||||
complete privacy and control over your documents.
|
||||
</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">Does it work on Mac, Windows, and Mobile?</span>
|
||||
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||
</button>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Yes! Since BentoPDF runs entirely in your browser, it works on any operating system with a
|
||||
modern web
|
||||
browser, including Windows, macOS, Linux, iOS, and Android.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New FAQ: GDPR Compliance -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||
<span class="text-lg font-semibold text-white">Is BentoPDF GDPR compliant?</span>
|
||||
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||
</button>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Yes. BentoPDF is fully GDPR compliant. Since all file processing happens locally in your browser
|
||||
and we
|
||||
never collect or transmit your files to any server, we have no access to your data. This ensures
|
||||
you are
|
||||
always in control of your documents.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New FAQ: Data Storage -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||
<span class="text-lg font-semibold text-white">Do you store or track any of my files?</span>
|
||||
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||
</button>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
No. We never store, track, or log your files. Everything you do on BentoPDF happens in your
|
||||
browser
|
||||
memory and disappears once you close the page. There are no uploads, no history logs, and no
|
||||
servers
|
||||
involved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New FAQ: Privacy -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||
<span class="text-lg font-semibold text-white">What makes BentoPDF different from other PDF
|
||||
tools?</span>
|
||||
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||
</button>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
Most PDF tools upload your files to a server for processing. BentoPDF never does that. We use
|
||||
secure,
|
||||
modern web technology to process your files directly in your browser. This means faster
|
||||
performance,
|
||||
stronger privacy, and complete peace of mind.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New FAQ: Browser-Based -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||
<span class="text-lg font-semibold text-white">How does browser-based processing keep me
|
||||
safe?</span>
|
||||
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||
</button>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
By running entirely inside your browser, BentoPDF ensures that your files never leave your
|
||||
device. This
|
||||
eliminates the risks of server hacks, data breaches, or unauthorized access. Your files remain
|
||||
yours—always.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New FAQ: Analytics -->
|
||||
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
|
||||
<button class="faq-question w-full flex justify-between items-center text-left p-6">
|
||||
<span class="text-lg font-semibold text-white">Do you use cookies or analytics to track me?</span>
|
||||
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
|
||||
</button>
|
||||
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
|
||||
<p class="p-6 pt-0 text-gray-400">
|
||||
We care about your privacy. BentoPDF does not track personal information. We use <a
|
||||
href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank"
|
||||
rel="noopener noreferrer">Simple Analytics</a> 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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="section-divider hide-section"></div>
|
||||
|
||||
<section id="testimonials-section" class="py-20 hide-section">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
|
||||
What Our <span class="marker-slanted">Users</span> Say
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
|
||||
<div class="testimonial-card">
|
||||
<div class="flex items-center mb-4">
|
||||
<div>
|
||||
<p class="font-bold text-white">Sarah L.</p>
|
||||
<div class="text-yellow-400 flex items-center text-sm">★★★★★</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-400">"This is the tool I've been searching for! It's fast, free, and I love that
|
||||
my confidential documents never get uploaded to some random server. A lifesaver for my freelance
|
||||
work."</p>
|
||||
</div>
|
||||
<div class="testimonial-card">
|
||||
<div class="flex items-center mb-4">
|
||||
<div>
|
||||
<p class="font-bold text-white">Mark Chen</p>
|
||||
<div class="text-yellow-400 flex items-center text-sm">★★★★★</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-400">"Finally, a PDF editor that just works. No ads, no sign-ups, no nonsense.
|
||||
The merge tool is surprisingly powerful. I've already bookmarked it on all my devices."</p>
|
||||
</div>
|
||||
<div class="testimonial-card">
|
||||
<div class="flex items-center mb-4">
|
||||
<div>
|
||||
<p class="font-bold text-white">Anonymous User A-35Z</p>
|
||||
<div class="text-yellow-400 flex items-center text-sm">★☆☆☆☆</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-400">"Terrible. It won't let me upload my files to the cloud. How is my Big Data
|
||||
Tech Overlord supposed to know I signed a permission slip for my kid's field trip? Useless for
|
||||
my data profile."</p>
|
||||
</div>
|
||||
<div class="testimonial-card">
|
||||
<div class="flex items-center mb-4">
|
||||
<div>
|
||||
<p class="font-bold text-white">Dr. Brickson</p>
|
||||
<div class="text-yellow-400 flex items-center text-sm">★★★★★</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-400">"As a researcher, data privacy is paramount. BentoPDF's client-side
|
||||
processing model is exactly what my institution recommends. It's robust, reliable, and secure. A
|
||||
fantastic resource."</p>
|
||||
</div>
|
||||
<div class="testimonial-card">
|
||||
<div class="flex items-center mb-4">
|
||||
<div>
|
||||
<p class="font-bold text-white">AdTracker Pro</p>
|
||||
<div class="text-yellow-400 flex items-center text-sm">★☆☆☆☆</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-400">"This website is broken. My ad blocker says it hasn't blocked a single
|
||||
tracker. How am I supposed to know if a product is good if it's not following me around the
|
||||
internet for a week? 1 star."</p>
|
||||
</div>
|
||||
<div class="testimonial-card">
|
||||
<div class="flex items-center mb-4">
|
||||
<div>
|
||||
<p class="font-bold text-white">Raj P.</p>
|
||||
<div class="text-yellow-400 flex items-center text-sm">★★★★★</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-400">"Simple, elegant, and powerful. I needed to merge 50 reports, and it
|
||||
handled it instantly without crashing my browser. This is what a web tool should be. Highly
|
||||
recommended."</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
<section id="support-section" class="py-20">
|
||||
<div
|
||||
class="max-w-4xl mx-auto text-center bg-gray-800 p-8 md:p-12 rounded-xl border border-gray-700 shadow-2xl">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||
Like My Work?
|
||||
</h2>
|
||||
<p class="text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||
BentoPDF is a passion project, built to provide a free, private, and powerful PDF toolkit for
|
||||
everyone. If you find it useful, consider supporting its development. Every coffee helps!
|
||||
</p>
|
||||
|
||||
<a href="https://ko-fi.com/alio01" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-3 px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold
|
||||
focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400
|
||||
hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5">
|
||||
<i data-lucide="coffee" class="w-7 h-7"></i>
|
||||
<span>Buy Me a Coffee</span>
|
||||
</a>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<div id="signature-ghost" class="hidden"></div>
|
||||
</div>
|
||||
|
||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="public/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3">
|
||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">© 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://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/"
|
||||
class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
||||
<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/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
11141
package-lock.json
generated
Normal file
11141
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
package.json
Normal file
43
package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "bento-pdf",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"obfuscate": "node scripts/build.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/blob-stream": "^0.1.33",
|
||||
"@types/html2canvas": "^0.5.35",
|
||||
"@types/pdfkit": "^0.17.3",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@types/utif": "^3.0.6",
|
||||
"ts-migrate": "^0.1.35",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.1.7",
|
||||
"vite-plugin-node-polyfills": "^0.24.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"blob-stream": "^0.1.3",
|
||||
"cropperjs": "^1.6.1",
|
||||
"heic2any": "^0.0.4",
|
||||
"html2canvas": "^1.4.1",
|
||||
"javascript-obfuscator": "^4.1.1",
|
||||
"jspdf": "^3.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide": "^0.545.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^5.4.296",
|
||||
"pdfkit": "^0.17.2",
|
||||
"sortablejs": "^1.15.6",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"terser": "^5.44.0",
|
||||
"tesseract.js": "^6.0.1",
|
||||
"tiff": "^7.1.2",
|
||||
"utif": "^3.1.0"
|
||||
}
|
||||
}
|
||||
187
privacy.html
Normal file
187
privacy.html
Normal file
@@ -0,0 +1,187 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Privacy Policy - BentoPDF</title>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="dist/styles.css">
|
||||
<link rel="icon" type="image/png" href="images/favicon.svg">
|
||||
<style>
|
||||
.legal-content h2 {
|
||||
@apply text-2xl font-bold text-white mt-8 mb-4;
|
||||
}
|
||||
|
||||
.legal-content h3 {
|
||||
@apply text-xl font-semibold text-indigo-400 mt-6 mb-3;
|
||||
}
|
||||
|
||||
.legal-content p {
|
||||
@apply mb-4 leading-relaxed text-gray-400;
|
||||
}
|
||||
|
||||
.legal-content ul {
|
||||
@apply list-disc list-inside mb-4 pl-4 text-gray-400;
|
||||
}
|
||||
|
||||
.legal-content a {
|
||||
@apply text-indigo-400 underline hover:text-indigo-300;
|
||||
}
|
||||
</style>
|
||||
</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">
|
||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
|
||||
<img src="images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8 mr-2">
|
||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||
</a>
|
||||
<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="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center gap-4">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
</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:contact@bentopdf.com">contact@bentopdf.com</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="BentoPDF 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://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
||||
<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>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
5
public/images/favicon.svg
Normal file
5
public/images/favicon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="86" height="166" viewBox="0 0 86 166" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M56.7848 80.9116L0 25.6211L24.8434 1.43151C26.8037 -0.47717 29.9814 -0.47717 31.9414 1.43151L85.177 53.2662L56.7848 80.9116Z" fill="#A5B4FC"/>
|
||||
<path d="M-1.30805e-05 80.7335L21.2939 101.467L42.5878 80.7335L21.2939 60L-1.30805e-05 80.7335Z" fill="#E0E7FF"/>
|
||||
<path d="M56.7848 83L81.6279 107.19C83.5881 109.098 83.5881 112.192 81.6279 114.101L28.3925 165.936L0 138.29L56.7848 83Z" fill="#6366F1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
56
scripts/build.js
Normal file
56
scripts/build.js
Normal file
@@ -0,0 +1,56 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const sourceDir = path.join(__dirname, '../unoptimized-js');
|
||||
const outputDir = path.join(__dirname, '../dist/js');
|
||||
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
function processFile(filePath) {
|
||||
const relativePath = path.relative(sourceDir, filePath);
|
||||
const outputPath = path.join(outputDir, relativePath);
|
||||
const outputDirForFile = path.dirname(outputPath);
|
||||
|
||||
// Create subdirectories in the output folder
|
||||
if (!fs.existsSync(outputDirForFile)) {
|
||||
fs.mkdirSync(outputDirForFile, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Obfuscate the file
|
||||
console.log(`Obfuscating: ${filePath}`);
|
||||
const obfuscateCmd = `npx javascript-obfuscator "${filePath}" --output "${outputPath}"`;
|
||||
execSync(obfuscateCmd);
|
||||
|
||||
// 2. Minify the obfuscated file using Terser
|
||||
console.log(`Minifying: ${outputPath}`);
|
||||
const minifyCmd = `npx terser "${outputPath}" -o "${outputPath}" --compress --mangle`;
|
||||
execSync(minifyCmd);
|
||||
|
||||
console.log(`Success: ${outputPath}`);
|
||||
} catch (error) {
|
||||
console.error(`Error processing file ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function processDirectory(dirPath) {
|
||||
fs.readdirSync(dirPath).forEach(file => {
|
||||
const filePath = path.join(dirPath, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
processDirectory(filePath);
|
||||
} else if (path.extname(filePath) === '.js') {
|
||||
if (filePath !== path.join(__dirname, 'build.js')) {
|
||||
processFile(filePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Starting JavaScript build process...');
|
||||
processDirectory(sourceDir);
|
||||
console.log('Build complete. Files are in the /dist/js folder.');
|
||||
358
src/css/styles.css
Normal file
358
src/css/styles.css
Normal file
@@ -0,0 +1,358 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background-color: #111827;
|
||||
/* bg-gray-900 */
|
||||
color: #d1d5db;
|
||||
/* text-gray-300 */
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||
border: 1px solid #374151;
|
||||
/* border-gray-700 */
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||
border-color: #4f46e5;
|
||||
/* indigo-600 */
|
||||
}
|
||||
|
||||
.btn {
|
||||
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Custom file input */
|
||||
input[type="file"]::file-selector-button {
|
||||
@apply bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors duration-200 mr-4;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
/* bg-gray-800 */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4f46e5;
|
||||
/* indigo-600 */
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4338ca;
|
||||
/* indigo-700 */
|
||||
}
|
||||
|
||||
/* Style for drag-and-drop placeholder */
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
border: 2px dashed #4f46e5;
|
||||
}
|
||||
|
||||
#embed-pdf-container>div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#tool-interface {
|
||||
color: #39A0ED;
|
||||
}
|
||||
|
||||
.page-thumbnail,
|
||||
#file-list > li {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.compare-viewer-wrapper.overlay-mode {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
overflow: auto;
|
||||
border: 2px solid #374151;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* This rule now ONLY applies to canvases in overlay mode */
|
||||
.compare-viewer-wrapper.overlay-mode canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.compare-viewer-wrapper.side-by-side-mode {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pdf-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: auto;
|
||||
height: 75vh;
|
||||
border: 2px solid #374151;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* This rule ensures canvases in side-by-side panels display at their natural rendered size. */
|
||||
.pdf-panel canvas {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
footer a {
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.marker {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.marker::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
/* height: 30px; */
|
||||
background-color: orange; /* Yellow marker color */
|
||||
z-index: -1;
|
||||
transform: skew(-20deg);
|
||||
}
|
||||
|
||||
.pill {
|
||||
background-color: #374151; /* bg-gray-700 */
|
||||
color: #d1d5db; /* text-gray-300 */
|
||||
padding: 8px 16px;
|
||||
border-radius: 9999px; /* rounded-full */
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
background-color: #4f46e5; /* indigo-600 */
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px; /* rounded-lg */
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background-color: #4338ca; /* indigo-700 */
|
||||
}
|
||||
.marker-text {
|
||||
background-color: rgba(255, 255, 0, 0.5); /* Yellow marker color */
|
||||
padding: 0 5px;
|
||||
}
|
||||
.feature-card {
|
||||
background-color: #1f2937; /* bg-gray-800 */
|
||||
padding: 24px;
|
||||
border-radius: 8px; /* rounded-lg */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
@apply text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors;
|
||||
}
|
||||
|
||||
.mobile-nav-link {
|
||||
@apply text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium transition-colors;
|
||||
}
|
||||
|
||||
.marker-slanted {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.marker-slanted::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 6px;
|
||||
background: linear-gradient(120deg, #6366f1, #8b5cf6);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #4f46e5, transparent); /* Fades from transparent to indigo and back */
|
||||
margin: 2rem auto; /* my-16 */
|
||||
max-width: 42rem; /* max-w-xl */
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 2rem; /* py-3 px-8 */
|
||||
border-radius: 0.5rem; /* rounded-lg */
|
||||
background-image: linear-gradient(to bottom, #6366f1, #4f46e5); /* from-indigo-500 to-indigo-600 */
|
||||
color: #ffffff; /* text-white */
|
||||
font-weight: 600; /* font-semibold */
|
||||
transition-property: all;
|
||||
transition-duration: 200ms;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-gradient:hover {
|
||||
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3), 0 4px 6px -4px rgba(79, 70, 229, 0.3); /* hover:shadow-xl hover:shadow-indigo-500/30 */
|
||||
transform: translateY(-0.25rem); /* hover:-translate-y-1 */
|
||||
}
|
||||
|
||||
.btn-gradient:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px #111827, 0 0 0 4px #818cf8; /* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
|
||||
}
|
||||
|
||||
.btn-gradient:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.solid-spinner {
|
||||
width: 64px; /* w-16 */
|
||||
height: 64px; /* h-16 */
|
||||
border: 5px solid #374151; /* border-gray-700 */
|
||||
border-bottom-color: #4f46e5; /* border-indigo-600 */
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#signature-ghost {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
pointer-events: none; /* Allows clicks to pass through to the canvas */
|
||||
opacity: 0.6;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* Highlight for selected signature in the saved list */
|
||||
.saved-signature.selected {
|
||||
border-color: #4f46e5; /* indigo-600 */
|
||||
box-shadow: 0 0 10px rgba(79, 70, 229, 0.5);
|
||||
}
|
||||
|
||||
/* Cursor change when hovering over a placed signature */
|
||||
#canvas-sign.movable {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#canvas-sign.resize-ns { cursor: ns-resize; }
|
||||
#canvas-sign.resize-ew { cursor: ew-resize; }
|
||||
#canvas-sign.resize-nesw { cursor: nesw-resize; }
|
||||
#canvas-sign.resize-nwse { cursor: nwse-resize; }
|
||||
|
||||
.faq-item.open .faq-question {
|
||||
color: #818cf8; /* indigo-400 */
|
||||
}
|
||||
|
||||
.faq-item.open .faq-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Testimonial Card Styles */
|
||||
.testimonial-card {
|
||||
background-color: #1f2937; /* bg-gray-800 */
|
||||
padding: 24px;
|
||||
border-radius: 0.75rem; /* rounded-xl */
|
||||
border: 1px solid #374151; /* border-gray-700 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Find the existing .pill rule: */
|
||||
.pill {
|
||||
background-color: #374151; /* bg-gray-700 */
|
||||
color: #d1d5db; /* text-gray-300 */
|
||||
padding: 8px 16px;
|
||||
border-radius: 9999px; /* rounded-full */
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* And REPLACE it with this: */
|
||||
.pill {
|
||||
background-color: #374151; /* bg-gray-700 */
|
||||
color: #d1d5db; /* text-gray-300 */
|
||||
/* Smaller padding and font size by default for mobile */
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 9999px; /* rounded-full */
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* On small screens (640px) and up, revert to the larger size */
|
||||
@media (min-width: 640px) {
|
||||
.pill {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure form-field-group contents don't overflow on small screens */
|
||||
.form-field-group .capitalize {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
#form-fields-container {
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.form-field-group {
|
||||
display: inline-block;
|
||||
width: 90%; /* Adjust as needed */
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#page-merge-preview {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
}
|
||||
273
src/js/canvasEditor.ts
Normal file
273
src/js/canvasEditor.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { showLoader, hideLoader, showAlert } from './ui.js';
|
||||
import { state } from './state.js';
|
||||
import { toolLogic } from './logic/index.js';
|
||||
import { icons, createIcons } from "lucide";
|
||||
|
||||
const editorState = {
|
||||
pdf: null,
|
||||
canvas: null,
|
||||
context: null,
|
||||
container: null,
|
||||
currentPageNum: 1,
|
||||
pageRendering: false,
|
||||
pageNumPending: null,
|
||||
scale: 1.0,
|
||||
pageSnapshot: null,
|
||||
isDrawing: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
cropBoxes: {},
|
||||
lastInteractionRect: null, // Used to store the rectangle from the last move event
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the best scale to fit the page within the container.
|
||||
* @param {PDFPageProxy} page - The PDF.js page object.
|
||||
*/
|
||||
function calculateFitScale(page: any) {
|
||||
const containerWidth = editorState.container.clientWidth;
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
return containerWidth / viewport.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a specific page of the PDF onto the canvas.
|
||||
* @param {number} num The page number to render.
|
||||
*/
|
||||
async function renderPage(num: any) {
|
||||
editorState.pageRendering = true;
|
||||
showLoader(`Loading page ${num}...`);
|
||||
|
||||
try {
|
||||
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') {
|
||||
editorState.scale = calculateFitScale(page);
|
||||
}
|
||||
|
||||
const viewport = page.getViewport({ scale: editorState.scale });
|
||||
editorState.canvas.height = viewport.height;
|
||||
editorState.canvas.width = viewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: editorState.context,
|
||||
viewport: viewport
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
editorState.pageSnapshot = editorState.context.getImageData(0, 0, editorState.canvas.width, editorState.canvas.height);
|
||||
redrawShapes();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error rendering page:", error);
|
||||
showAlert('Render Error', 'Could not display the page.');
|
||||
} finally {
|
||||
editorState.pageRendering = false;
|
||||
hideLoader();
|
||||
|
||||
document.getElementById('current-page-display').textContent = num;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page').disabled = num <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page').disabled = num >= editorState.pdf.numPages;
|
||||
|
||||
if (editorState.pageNumPending !== null) {
|
||||
const pendingPage = editorState.pageNumPending;
|
||||
editorState.pageNumPending = null;
|
||||
queueRenderPage(pendingPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function queueRenderPage(num: any) {
|
||||
if (editorState.pageRendering) {
|
||||
editorState.pageNumPending = num;
|
||||
} else {
|
||||
editorState.currentPageNum = num;
|
||||
renderPage(num);
|
||||
}
|
||||
}
|
||||
|
||||
function redrawShapes() {
|
||||
if (editorState.pageSnapshot) {
|
||||
editorState.context.putImageData(editorState.pageSnapshot, 0, 0);
|
||||
}
|
||||
|
||||
const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||
if (currentCropBox) {
|
||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||
editorState.context.lineWidth = 2;
|
||||
editorState.context.setLineDash([8, 4]);
|
||||
editorState.context.strokeRect(currentCropBox.x, currentCropBox.y, currentCropBox.width, currentCropBox.height);
|
||||
editorState.context.setLineDash([]);
|
||||
}
|
||||
}
|
||||
|
||||
function getEventCoordinates(e: any) {
|
||||
const rect = editorState.canvas.getBoundingClientRect();
|
||||
const touch = e.touches ? e.touches[0] : e;
|
||||
const scaleX = editorState.canvas.width / rect.width;
|
||||
const scaleY = editorState.canvas.height / rect.height;
|
||||
return {
|
||||
x: (touch.clientX - rect.left) * scaleX,
|
||||
y: (touch.clientY - rect.top) * scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
function handleInteractionStart(e: any) {
|
||||
e.preventDefault();
|
||||
const coords = getEventCoordinates(e);
|
||||
editorState.isDrawing = true;
|
||||
editorState.startX = coords.x;
|
||||
editorState.startY = coords.y;
|
||||
}
|
||||
|
||||
function handleInteractionMove(e: any) {
|
||||
if (!editorState.isDrawing) return;
|
||||
e.preventDefault();
|
||||
|
||||
redrawShapes();
|
||||
const coords = getEventCoordinates(e);
|
||||
|
||||
const x = Math.min(editorState.startX, coords.x);
|
||||
const y = Math.min(editorState.startY, coords.y);
|
||||
const width = Math.abs(editorState.startX - coords.x);
|
||||
const height = Math.abs(editorState.startY - coords.y);
|
||||
|
||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||
editorState.context.lineWidth = 2;
|
||||
editorState.context.setLineDash([8, 4]);
|
||||
editorState.context.strokeRect(x, y, width, height);
|
||||
editorState.context.setLineDash([]);
|
||||
|
||||
// Store the last valid rectangle drawn during the move event
|
||||
editorState.lastInteractionRect = { x, y, width, height };
|
||||
}
|
||||
|
||||
function handleInteractionEnd() {
|
||||
if (!editorState.isDrawing) return;
|
||||
editorState.isDrawing = false;
|
||||
|
||||
const finalRect = editorState.lastInteractionRect;
|
||||
|
||||
if (!finalRect || finalRect.width < 5 || finalRect.height < 5) {
|
||||
redrawShapes(); // Redraw to clear any invalid, tiny box
|
||||
editorState.lastInteractionRect = null;
|
||||
return;
|
||||
}
|
||||
|
||||
editorState.cropBoxes[editorState.currentPageNum - 1] = {
|
||||
...finalRect,
|
||||
scale: editorState.scale
|
||||
};
|
||||
|
||||
editorState.lastInteractionRect = null; // Reset for the next drawing action
|
||||
redrawShapes();
|
||||
}
|
||||
|
||||
export async function setupCanvasEditor(toolId: any) {
|
||||
editorState.canvas = document.getElementById('canvas-editor');
|
||||
if (!editorState.canvas) return;
|
||||
editorState.container = document.getElementById('canvas-container');
|
||||
editorState.context = editorState.canvas.getContext('2d');
|
||||
|
||||
const pageNav = document.getElementById('page-nav');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
editorState.cropBoxes = {};
|
||||
editorState.currentPageNum = 1;
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
|
||||
editorState.scale = 'fit';
|
||||
|
||||
pageNav.textContent = '';
|
||||
|
||||
const prevButton = document.createElement('button');
|
||||
prevButton.id = 'prev-page';
|
||||
prevButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
|
||||
|
||||
const pageInfo = document.createElement('span');
|
||||
pageInfo.className = 'text-white font-medium';
|
||||
|
||||
const currentPageDisplay = document.createElement('span');
|
||||
currentPageDisplay.id = 'current-page-display';
|
||||
currentPageDisplay.textContent = '1';
|
||||
|
||||
pageInfo.append('Page ', currentPageDisplay, ` of ${editorState.pdf.numPages}`);
|
||||
|
||||
const nextButton = document.createElement('button');
|
||||
nextButton.id = 'next-page';
|
||||
nextButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
||||
|
||||
pageNav.append(prevButton, pageInfo, nextButton);
|
||||
|
||||
createIcons({icons});
|
||||
|
||||
document.getElementById('prev-page').addEventListener('click', () => {
|
||||
if (editorState.currentPageNum > 1) queueRenderPage(editorState.currentPageNum - 1);
|
||||
});
|
||||
document.getElementById('next-page').addEventListener('click', () => {
|
||||
if (editorState.currentPageNum < editorState.pdf.numPages) queueRenderPage(editorState.currentPageNum + 1);
|
||||
});
|
||||
|
||||
// To prevent stacking multiple listeners, we replace the canvas element with a clone
|
||||
const newCanvas = editorState.canvas.cloneNode(true);
|
||||
editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas);
|
||||
editorState.canvas = newCanvas;
|
||||
editorState.context = newCanvas.getContext('2d');
|
||||
|
||||
// Mouse Events
|
||||
editorState.canvas.addEventListener('mousedown', handleInteractionStart);
|
||||
editorState.canvas.addEventListener('mousemove', handleInteractionMove);
|
||||
editorState.canvas.addEventListener('mouseup', handleInteractionEnd);
|
||||
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
|
||||
|
||||
// Touch Events
|
||||
editorState.canvas.addEventListener('touchstart', handleInteractionStart, { passive: false });
|
||||
editorState.canvas.addEventListener('touchmove', handleInteractionMove, { passive: false });
|
||||
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
||||
|
||||
if (toolId === 'crop') {
|
||||
document.getElementById('zoom-in-btn').onclick = () => {
|
||||
editorState.scale += 0.25;
|
||||
renderPage(editorState.currentPageNum);
|
||||
};
|
||||
document.getElementById('zoom-out-btn').onclick = () => {
|
||||
if (editorState.scale > 0.25) {
|
||||
editorState.scale -= 0.25;
|
||||
renderPage(editorState.currentPageNum);
|
||||
}
|
||||
};
|
||||
document.getElementById('fit-page-btn').onclick = async () => {
|
||||
const page = await editorState.pdf.getPage(editorState.currentPageNum);
|
||||
editorState.scale = calculateFitScale(page);
|
||||
renderPage(editorState.currentPageNum);
|
||||
};
|
||||
document.getElementById('clear-crop-btn').onclick = () => {
|
||||
delete editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||
redrawShapes();
|
||||
};
|
||||
document.getElementById('clear-all-crops-btn').onclick = () => {
|
||||
editorState.cropBoxes = {};
|
||||
redrawShapes();
|
||||
};
|
||||
|
||||
document.getElementById('process-btn').onclick = async () => {
|
||||
if (Object.keys(editorState.cropBoxes).length === 0) {
|
||||
showAlert('No Area Selected', 'Please draw a rectangle on at least one page to select the crop area.');
|
||||
return;
|
||||
}
|
||||
const success = await toolLogic['crop-pdf'].process(editorState.cropBoxes);
|
||||
if (success) {
|
||||
showAlert('Success!', 'Your PDF has been cropped and the download has started.');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
queueRenderPage(1);
|
||||
}
|
||||
22
src/js/config/tesseract-languages.ts
Normal file
22
src/js/config/tesseract-languages.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const tesseractLanguages = {
|
||||
"eng": "English", "afr": "Afrikaans", "amh": "Amharic", "ara": "Arabic", "asm": "Assamese", "aze": "Azerbaijani",
|
||||
"aze_cyrl": "Azerbaijani - Cyrillic", "bel": "Belarusian", "ben": "Bengali", "bod": "Tibetan", "bos": "Bosnian",
|
||||
"bul": "Bulgarian", "cat": "Catalan; Valencian", "ceb": "Cebuano", "ces": "Czech", "chi_sim": "Chinese - Simplified",
|
||||
"chi_tra": "Chinese - Traditional", "chr": "Cherokee", "cym": "Welsh", "dan": "Danish", "deu": "German",
|
||||
"dzo": "Dzongkha", "ell": "Greek, Modern (1453-)", "enm": "English, Middle (1100-1500)", "epo": "Esperanto",
|
||||
"est": "Estonian", "eus": "Basque", "fas": "Persian", "fin": "Finnish", "fra": "French", "frk": "German Fraktur",
|
||||
"frm": "French, Middle (ca. 1400-1600)", "gle": "Irish", "glg": "Galician", "grc": "Greek, Ancient (-1453)",
|
||||
"guj": "Gujarati", "hat": "Haitian; Haitian Creole", "heb": "Hebrew", "hin": "Hindi", "hrv": "Croatian",
|
||||
"hun": "Hungarian", "iku": "Inuktitut", "ind": "Indonesian", "isl": "Icelandic", "ita": "Italian",
|
||||
"ita_old": "Italian - Old", "jav": "Javanese", "jpn": "Japanese", "kan": "Kannada", "kat": "Georgian",
|
||||
"kat_old": "Georgian - Old", "kaz": "Kazakh", "khm": "Central Khmer", "kir": "Kirghiz; Kyrgyz",
|
||||
"kor": "Korean", "kur": "Kurdish", "lao": "Lao", "lat": "Latin", "lav": "Latvian", "lit": "Lithuanian",
|
||||
"mal": "Malayalam", "mar": "Marathi", "mkd": "Macedonian", "mlt": "Maltese", "msa": "Malay", "mya": "Burmese",
|
||||
"nep": "Nepali", "nld": "Dutch; Flemish", "nor": "Norwegian", "ori": "Oriya", "pan": "Panjabi; Punjabi",
|
||||
"pol": "Polish", "por": "Portuguese", "pus": "Pushto; Pashto", "ron": "Romanian; Moldavian; Moldovan",
|
||||
"rus": "Russian", "san": "Sanskrit", "sin": "Sinhala; Sinhalese", "slk": "Slovak", "slv": "Slovenian",
|
||||
"spa": "Spanish; Castilian", "spa_old": "Spanish; Castilian - Old", "sqi": "Albanian", "srp": "Serbian",
|
||||
"srp_latn": "Serbian - Latin", "swa": "Swahili", "swe": "Swedish", "syr": "Syriac", "tam": "Tamil", "tel": "Telugu",
|
||||
"tgk": "Tajik", "tgl": "Tagalog", "tha": "Thai", "tir": "Tigrinya", "tur": "Turkish", "uig": "Uighur; Uyghur",
|
||||
"ukr": "Ukrainian", "urd": "Urdu", "uzb": "Uzbek", "uzb_cyrl": "Uzbek - Cyrillic", "vie": "Vietnamese", "yid": "Yiddish"
|
||||
};
|
||||
104
src/js/config/tools.ts
Normal file
104
src/js/config/tools.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
// This file centralizes the definition of all available tools, organized by category.
|
||||
export const categories = [
|
||||
{
|
||||
name: 'Popular Tools',
|
||||
tools: [
|
||||
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
|
||||
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
|
||||
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
|
||||
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs' },
|
||||
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
|
||||
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
|
||||
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
||||
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
|
||||
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
|
||||
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Edit & Annotate',
|
||||
tools: [
|
||||
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.' },
|
||||
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
||||
{ id: 'add-page-numbers', name: 'Page Numbers', icon: 'list-ordered', subtitle: 'Insert page numbers into your document.' },
|
||||
{ id: 'add-watermark', name: 'Add Watermark', icon: 'droplets', subtitle: 'Stamp text or an image over your PDF pages.' },
|
||||
{ id: 'add-header-footer', name: 'Header & Footer', icon: 'pilcrow', subtitle: 'Add text to the top and bottom of pages.' },
|
||||
{ id: 'invert-colors', name: 'Invert Colors', icon: 'contrast', subtitle: 'Create a "dark mode" version of your PDF.' },
|
||||
{ id: 'change-background-color', name: 'Background Color', icon: 'palette', subtitle: 'Change the background color of your PDF.' },
|
||||
{ id: 'change-text-color', name: 'Change Text Color', icon: 'type', subtitle: 'Change the color of text in your PDF.' },
|
||||
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
|
||||
{ id: 'remove-annotations', name: 'Remove Annotations', icon: 'eraser', subtitle: 'Strip comments, highlights, and links.' },
|
||||
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
||||
{ id: 'form-filler', name: 'PDF Form Filler', icon: 'square-pen', subtitle: 'Fill in forms directly in the browser.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Convert to PDF',
|
||||
tools: [
|
||||
{ id: 'image-to-pdf', name: 'Image to PDF', icon: 'images', subtitle: 'Combine various images into one PDF.' },
|
||||
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
|
||||
{ id: 'png-to-pdf', name: 'PNG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more PNG images.' },
|
||||
{ id: 'webp-to-pdf', name: 'WebP to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more WebP images.' },
|
||||
{ id: 'svg-to-pdf', name: 'SVG to PDF', icon: 'pen-tool', subtitle: 'Create a PDF from one or more SVG images.' },
|
||||
{ id: 'bmp-to-pdf', name: 'BMP to PDF', icon: 'image', subtitle: 'Create a PDF from one or more BMP images.' },
|
||||
{ id: 'heic-to-pdf', name: 'HEIC to PDF', icon: 'smartphone', subtitle: 'Create a PDF from one or more HEIC images.' },
|
||||
{ id: 'tiff-to-pdf', name: 'TIFF to PDF', icon: 'layers', subtitle: 'Create a PDF from one or more TIFF images.' },
|
||||
{ id: 'txt-to-pdf', name: 'Text to PDF', icon: 'file-pen', subtitle: 'Convert a plain text file into a PDF.' },
|
||||
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
|
||||
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
|
||||
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Convert from PDF',
|
||||
tools: [
|
||||
{ id: 'pdf-to-jpg', name: 'PDF to JPG', icon: 'file-image', subtitle: 'Convert each PDF page into a JPG image.' },
|
||||
{ id: 'pdf-to-png', name: 'PDF to PNG', icon: 'file-image', subtitle: 'Convert each PDF page into a PNG image.' },
|
||||
{ id: 'pdf-to-webp', name: 'PDF to WebP', icon: 'file-image', subtitle: 'Convert each PDF page into a WebP image.' },
|
||||
{ id: 'pdf-to-bmp', name: 'PDF to BMP', icon: 'file-image', subtitle: 'Convert each PDF page into a BMP image.' },
|
||||
{ id: 'pdf-to-tiff', name: 'PDF to TIFF', icon: 'file-image', subtitle: 'Convert each PDF page into a TIFF image.' },
|
||||
{ id: 'pdf-to-greyscale', name: 'PDF to Greyscale', icon: 'palette', subtitle: 'Convert all colors to black and white.' },
|
||||
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Organize & Manage',
|
||||
tools: [
|
||||
{ id: 'ocr-pdf', name: 'OCR PDF', icon: 'scan-text', subtitle: 'Make a PDF searchable and copyable.' },
|
||||
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
|
||||
{ id: 'organize', name: 'Organize PDF', icon: 'grip', subtitle: 'Reorder pages by dragging and dropping.' },
|
||||
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
|
||||
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
|
||||
{ id: 'split-in-half', name: 'Divide Pages', icon: 'table-columns-split', subtitle: 'Divide pages horizontally or vertically.' },
|
||||
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
|
||||
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
|
||||
{ id: 'add-blank-page', name: 'Add Blank Page', icon: 'file-plus-2', subtitle: 'Insert an empty page anywhere in your PDF.' },
|
||||
{ id: 'reverse-pages', name: 'Reverse Pages', icon: 'arrow-down-z-a', subtitle: 'Flip the order of all pages in your document.' },
|
||||
{ id: 'rotate', name: 'Rotate PDF', icon: 'rotate-cw', subtitle: 'Turn pages in 90-degree increments.' },
|
||||
{ id: 'n-up', name: 'N-Up PDF', icon: 'layout-grid', subtitle: 'Arrange multiple pages onto a single sheet.' },
|
||||
{ id: 'combine-single-page', name: 'Combine to Single Page', icon: 'unfold-vertical', subtitle: 'Stitch all pages into one continuous scroll.' },
|
||||
{ id: 'view-metadata', name: 'View Metadata', icon: 'info', subtitle: 'Inspect the hidden properties of your PDF.' },
|
||||
{ id: 'edit-metadata', name: 'Edit Metadata', icon: 'file-cog', subtitle: 'Change the author, title, and other properties.' },
|
||||
{ id: 'pdf-to-zip', name: 'PDFs to ZIP', icon: 'stretch-horizontal', subtitle: 'Package multiple PDF files into a ZIP archive.' },
|
||||
{ id: 'compare-pdfs', name: 'Compare PDFs', icon: 'git-compare', subtitle: 'Compare two PDFs side by side.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Optimize & Repair',
|
||||
tools: [
|
||||
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
|
||||
{ id: 'fix-dimensions', name: 'Fix Page Size', icon: 'ruler-dimension-line', subtitle: 'Standardize all pages to a uniform size.' },
|
||||
{ id: 'page-dimensions', name: 'Page Dimensions', icon: 'ruler', subtitle: 'Analyze page size, orientation, and units.' },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Secure PDF',
|
||||
tools: [
|
||||
{ id: 'encrypt', name: 'Encrypt PDF', icon: 'lock', subtitle: 'Add a password to protect your PDF.' },
|
||||
{ id: 'decrypt', name: 'Decrypt PDF', icon: 'unlock', subtitle: 'Remove password protection from a PDF.' },
|
||||
{ id: 'flatten', name: 'Flatten PDF', icon: 'layers', subtitle: 'Make form fields and annotations non-editable.' },
|
||||
{ id: 'remove-metadata', name: 'Remove Metadata', icon: 'file-x', subtitle: 'Strip hidden data from your PDF.' },
|
||||
{ id: 'change-permissions', name: 'Change Permissions', icon: 'shield-check', subtitle: 'Set or change user permissions on a PDF.' },
|
||||
]
|
||||
},
|
||||
];
|
||||
613
src/js/handlers/fileHandler.ts
Normal file
613
src/js/handlers/fileHandler.ts
Normal file
@@ -0,0 +1,613 @@
|
||||
// FILE: js/handlers/fileHandler.js
|
||||
|
||||
import { state } from '../state.js';
|
||||
import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js';
|
||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { setupCanvasEditor } from '../canvasEditor.js';
|
||||
import { toolLogic } from '../logic/index.js';
|
||||
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { icons, createIcons } from 'lucide';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
async function handleSinglePdfUpload(toolId: any, file: any) {
|
||||
showLoader('Loading PDF...');
|
||||
try {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
hideLoader();
|
||||
|
||||
if (state.pdfDoc.isEncrypted && toolId !== 'decrypt' && toolId !== 'change-permissions') {
|
||||
showAlert('Protected PDF', 'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.');
|
||||
switchView('grid');
|
||||
return;
|
||||
}
|
||||
|
||||
const optionsDiv = document.querySelector('[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]');
|
||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
processBtn.disabled = false;
|
||||
processBtn.classList.remove('hidden');
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
processBtn.onclick = func;
|
||||
}
|
||||
}
|
||||
|
||||
if (['split', 'delete-pages', 'add-blank-page', 'extract-pages', 'add-header-footer'].includes(toolId)) {
|
||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount();
|
||||
}
|
||||
|
||||
if (toolId === 'organize' || toolId === 'rotate') {
|
||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||
|
||||
if (toolId === 'rotate') {
|
||||
const rotateAllControls = document.getElementById('rotate-all-controls');
|
||||
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
||||
const rotateAllRightBtn = document.getElementById('rotate-all-right-btn');
|
||||
|
||||
// Show the buttons
|
||||
rotateAllControls.classList.remove('hidden');
|
||||
createIcons({icons}); // Render the new icons
|
||||
|
||||
const rotateAll = (direction: any) => {
|
||||
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const currentRotation = parseInt(item.dataset.rotation || '0');
|
||||
// Calculate new rotation, ensuring it wraps around 0-270 degrees
|
||||
const newRotation = (currentRotation + (direction * 90) + 360) % 360;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
item.dataset.rotation = newRotation;
|
||||
|
||||
const thumbnail = item.querySelector('canvas, img');
|
||||
if (thumbnail) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'style' does not exist on type 'Element'.
|
||||
thumbnail.style.transform = `rotate(${newRotation}deg)`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
rotateAllLeftBtn.onclick = () => rotateAll(-1); // -1 for counter-clockwise
|
||||
rotateAllRightBtn.onclick = () => rotateAll(1); // 1 for clockwise
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (toolId === 'duplicate-organize') {
|
||||
await renderDuplicateOrganizeThumbnails();
|
||||
}
|
||||
if (['crop', 'redact'].includes(toolId)) {
|
||||
await setupCanvasEditor(toolId);
|
||||
}
|
||||
|
||||
if (toolId === 'view-metadata') {
|
||||
const resultsDiv = document.getElementById('metadata-results');
|
||||
showLoader('Analyzing full PDF metadata...');
|
||||
|
||||
try {
|
||||
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
|
||||
const [metadata, fieldObjects] = await Promise.all([
|
||||
pdfjsDoc.getMetadata(),
|
||||
pdfjsDoc.getFieldObjects()
|
||||
]);
|
||||
|
||||
const { info, metadata: rawXmpString } = metadata;
|
||||
|
||||
const parsePdfDate = (pdfDate: any) => {
|
||||
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) return pdfDate;
|
||||
try {
|
||||
const year = pdfDate.substring(2, 6);
|
||||
const month = pdfDate.substring(6, 8);
|
||||
const day = pdfDate.substring(8, 10);
|
||||
const hour = pdfDate.substring(10, 12);
|
||||
const minute = pdfDate.substring(12, 14);
|
||||
const second = pdfDate.substring(14, 16);
|
||||
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
|
||||
} catch {
|
||||
return pdfDate;
|
||||
}
|
||||
};
|
||||
|
||||
let htmlContent = '';
|
||||
|
||||
htmlContent += `
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold text-white mb-2">Info Dictionary</h3>
|
||||
<ul class="space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700">`;
|
||||
|
||||
if (info && Object.keys(info).length > 0) {
|
||||
for (const key in info) {
|
||||
let value = info[key];
|
||||
let displayValue;
|
||||
|
||||
if (value === null || value === undefined || String(value).trim() === '') {
|
||||
displayValue = `<span class="text-gray-500 italic">- Not Set -</span>`;
|
||||
} else if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
|
||||
displayValue = `<span class="text-white break-all">${parsePdfDate(value)}</span>`;
|
||||
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
try {
|
||||
let nestedList = '<ul class="mt-2 space-y-1 pl-4 border-l border-gray-600">';
|
||||
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
||||
nestedList += `<li class="flex"><strong class="w-32 flex-shrink-0 text-gray-500">${nestedKey}:</strong> <span class="text-white break-all">${nestedValue}</span></li>`;
|
||||
}
|
||||
nestedList += '</ul>';
|
||||
displayValue = nestedList;
|
||||
} catch (e) {
|
||||
displayValue = `<span class="text-white break-all">[Unserializable Object]</span>`;
|
||||
}
|
||||
} else {
|
||||
// Fallback for simple values (strings, numbers, arrays)
|
||||
displayValue = `<span class="text-white break-all">${String(value)}</span>`;
|
||||
}
|
||||
htmlContent += `<li class="flex flex-col sm:flex-row"><strong class="w-40 flex-shrink-0 text-gray-400">${key}</strong><div class="flex-grow">${displayValue}</div></li>`;
|
||||
}
|
||||
} else {
|
||||
htmlContent += `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||
}
|
||||
htmlContent += `</ul></div>`;
|
||||
|
||||
htmlContent += `
|
||||
<div class="mb-4">
|
||||
<h3 class="text-lg font-semibold text-white mb-2">Interactive Form Fields</h3>
|
||||
<ul class="space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700">`;
|
||||
|
||||
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
||||
const getFriendlyFieldType = (type: any) => {
|
||||
switch (type) {
|
||||
case 'Tx': return 'Text'; case 'Btn': return 'Button/Checkbox';
|
||||
case 'Ch': return 'Choice/Dropdown'; case 'Sig': return 'Signature';
|
||||
default: return type || 'Unknown';
|
||||
}
|
||||
};
|
||||
for (const fieldName in fieldObjects) {
|
||||
const field = fieldObjects[fieldName][0];
|
||||
const fieldType = getFriendlyFieldType(field.fieldType);
|
||||
const fieldValue = field.fieldValue ? `<span class="text-white break-all">${field.fieldValue}</span>` : `<span class="text-gray-500 italic">- Not Set -</span>`;
|
||||
htmlContent += `
|
||||
<li class="flex flex-col sm:flex-row">
|
||||
<strong class="w-40 flex-shrink-0 text-gray-400 font-semibold">${fieldName}</strong>
|
||||
<div><span class="text-gray-300 mr-2">[${fieldType}]</span>${fieldValue}</div>
|
||||
</li>`;
|
||||
}
|
||||
} else {
|
||||
htmlContent += `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
||||
}
|
||||
htmlContent += `</ul></div>`;
|
||||
|
||||
htmlContent += `
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-white mb-2">XMP Metadata (Raw XML)</h3>
|
||||
<div class="bg-gray-900 p-4 rounded-lg border border-gray-700">`;
|
||||
|
||||
if (rawXmpString) {
|
||||
const escapedXml = rawXmpString.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
htmlContent += `<pre class="text-xs text-gray-300 whitespace-pre-wrap break-all">${escapedXml}</pre>`;
|
||||
} else {
|
||||
htmlContent += `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
||||
}
|
||||
htmlContent += `</div></div>`;
|
||||
|
||||
resultsDiv.innerHTML = htmlContent;
|
||||
resultsDiv.classList.remove('hidden');
|
||||
|
||||
} catch (e) {
|
||||
console.error("Failed to view metadata or fields:", e);
|
||||
showAlert('Error', 'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
if (toolId === 'edit-metadata') {
|
||||
const form = document.getElementById('metadata-form');
|
||||
const formatDateForInput = (date: any) => {
|
||||
if (!date) return '';
|
||||
const pad = (num: any) => num.toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-title').value = state.pdfDoc.getTitle() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-author').value = state.pdfDoc.getAuthor() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-subject').value = state.pdfDoc.getSubject() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-keywords').value = state.pdfDoc.getKeywords() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-creator').value = state.pdfDoc.getCreator() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-producer').value = state.pdfDoc.getProducer() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-creation-date').value = formatDateForInput(state.pdfDoc.getCreationDate());
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-mod-date').value = formatDateForInput(state.pdfDoc.getModificationDate());
|
||||
|
||||
form.classList.remove('hidden');
|
||||
|
||||
const addBtn = document.getElementById('add-custom-meta-btn');
|
||||
const container = document.getElementById('custom-metadata-container');
|
||||
|
||||
addBtn.onclick = () => {
|
||||
const newFieldId = `custom-field-${Date.now()}`;
|
||||
const fieldWrapper = document.createElement('div');
|
||||
fieldWrapper.id = newFieldId;
|
||||
fieldWrapper.className = 'flex items-center gap-2';
|
||||
fieldWrapper.innerHTML = `
|
||||
<input type="text" placeholder="Key (e.g., Department)" class="custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2">
|
||||
<input type="text" placeholder="Value (e.g., Marketing)" class="custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2">
|
||||
<button type="button" class="btn p-2 text-red-500 hover:bg-gray-700 rounded-full" onclick="document.getElementById('${newFieldId}').remove()">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(fieldWrapper);
|
||||
|
||||
createIcons({icons}); // Re-render icons
|
||||
};
|
||||
|
||||
createIcons({icons});
|
||||
}
|
||||
if (toolId === 'edit-metadata') {
|
||||
const form = document.getElementById('metadata-form');
|
||||
const container = document.getElementById('custom-metadata-container');
|
||||
const addBtn = document.getElementById('add-custom-meta-btn');
|
||||
|
||||
// Helper to format Date objects
|
||||
const formatDateForInput = (date: any) => {
|
||||
if (!date) return '';
|
||||
const pad = (num: any) => num.toString().padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Comprehensive decoder for PDF values
|
||||
const decodePDFValue = (valueNode: any) => {
|
||||
if (!valueNode) return '';
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'PDFLib' does not exist on type 'Window &... Remove this comment to see the full error message
|
||||
const { PDFHexString, PDFString, PDFName, PDFNumber } = window.PDFLib;
|
||||
|
||||
try {
|
||||
// Handle PDFHexString
|
||||
if (valueNode instanceof PDFHexString) {
|
||||
// Try the built-in decoder first
|
||||
try {
|
||||
return valueNode.decodeText();
|
||||
} catch (e) {
|
||||
console.warn('Built-in decodeText failed for PDFHexString, trying manual decode');
|
||||
// Manual hex decoding
|
||||
const hexStr = valueNode.toString();
|
||||
return decodeHexStringManual(hexStr);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle PDFString
|
||||
if (valueNode instanceof PDFString) {
|
||||
try {
|
||||
return valueNode.decodeText();
|
||||
} catch (e) {
|
||||
console.warn('Built-in decodeText failed for PDFString, using toString');
|
||||
return valueNode.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle other types
|
||||
if (valueNode instanceof PDFName) {
|
||||
return valueNode.decodeText ? valueNode.decodeText() : valueNode.toString();
|
||||
}
|
||||
|
||||
if (valueNode instanceof PDFNumber) {
|
||||
return valueNode.toString();
|
||||
}
|
||||
|
||||
// Fallback - check if the toString() result needs hex decoding
|
||||
const strValue = valueNode.toString();
|
||||
|
||||
// Check for various hex encoding patterns
|
||||
if (strValue.includes('#')) {
|
||||
// Pattern like "helllo#20h"
|
||||
return strValue.replace(/#([0-9A-Fa-f]{2})/g, (match: any, hex: any) => {
|
||||
return String.fromCharCode(parseInt(hex, 16));
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it's a hex string in angle brackets like <48656C6C6C6F20>
|
||||
if (strValue.match(/^<[0-9A-Fa-f\s]+>$/)) {
|
||||
return decodeHexStringManual(strValue);
|
||||
}
|
||||
|
||||
// Check if it's a parentheses-wrapped string
|
||||
if (strValue.match(/^\([^)]*\)$/)) {
|
||||
return strValue.slice(1, -1); // Remove parentheses
|
||||
}
|
||||
|
||||
return strValue;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error decoding PDF value:', error);
|
||||
return valueNode.toString();
|
||||
}
|
||||
};
|
||||
|
||||
// Manual hex string decoder
|
||||
const decodeHexStringManual = (hexStr: any) => {
|
||||
try {
|
||||
// Remove angle brackets if present
|
||||
let cleanHex = hexStr.replace(/^<|>$/g, '');
|
||||
// Remove any whitespace
|
||||
cleanHex = cleanHex.replace(/\s/g, '');
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < cleanHex.length; i += 2) {
|
||||
const hexPair = cleanHex.substr(i, 2);
|
||||
if (hexPair.length === 2 && /^[0-9A-Fa-f]{2}$/.test(hexPair)) {
|
||||
const charCode = parseInt(hexPair, 16);
|
||||
// Only add printable characters or common whitespace
|
||||
if (charCode >= 32 && charCode <= 126) {
|
||||
result += String.fromCharCode(charCode);
|
||||
} else if (charCode === 10 || charCode === 13 || charCode === 9) {
|
||||
result += String.fromCharCode(charCode);
|
||||
} else {
|
||||
// For non-printable characters, you might want to skip or use a placeholder
|
||||
result += String.fromCharCode(charCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Manual hex decode failed:', error);
|
||||
return hexStr;
|
||||
}
|
||||
};
|
||||
|
||||
// --- 1. Pre-fill Standard Fields ---
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-title').value = state.pdfDoc.getTitle() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-author').value = state.pdfDoc.getAuthor() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-subject').value = state.pdfDoc.getSubject() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-keywords').value = state.pdfDoc.getKeywords() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-creator').value = state.pdfDoc.getCreator() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-producer').value = state.pdfDoc.getProducer() || '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-creation-date').value = formatDateForInput(state.pdfDoc.getCreationDate());
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('meta-mod-date').value = formatDateForInput(state.pdfDoc.getModificationDate());
|
||||
|
||||
container.querySelectorAll('.custom-field-wrapper').forEach(el => el.remove());
|
||||
|
||||
addBtn.onclick = () => {
|
||||
const newFieldId = `custom-field-${Date.now()}`;
|
||||
const fieldWrapper = document.createElement('div');
|
||||
fieldWrapper.id = newFieldId;
|
||||
fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper';
|
||||
fieldWrapper.innerHTML = `
|
||||
<input type="text" placeholder="Key (e.g., Department)" class="custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2">
|
||||
<input type="text" placeholder="Value (e.g., Marketing)" class="custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2">
|
||||
<button type="button" class="btn p-2 text-red-500 hover:bg-gray-700 rounded-full" onclick="document.getElementById('${newFieldId}').remove()">
|
||||
<i data-lucide="trash-2"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(fieldWrapper);
|
||||
|
||||
createIcons({icons});
|
||||
};
|
||||
|
||||
form.classList.remove('hidden');
|
||||
|
||||
createIcons({icons}); // Render all icons after dynamic changes
|
||||
}
|
||||
|
||||
if (toolId === 'cropper') {
|
||||
document.getElementById('cropper-ui-container').classList.remove('hidden');
|
||||
}
|
||||
|
||||
if (toolId === 'page-dimensions') {
|
||||
toolLogic['page-dimensions']();
|
||||
}
|
||||
|
||||
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
||||
toolLogic[toolId].setup();
|
||||
}
|
||||
} catch (e) {
|
||||
hideLoader();
|
||||
showAlert('Error', 'Could not load PDF. The file may be invalid, corrupted, or password-protected.');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMultiFileUpload(toolId: any) {
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
processBtn.disabled = false;
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
processBtn.onclick = func;
|
||||
}
|
||||
}
|
||||
|
||||
if (toolId === 'merge') {
|
||||
toolLogic.merge.setup();
|
||||
} else if (toolId === 'image-to-pdf') {
|
||||
const imageList = document.getElementById('image-list');
|
||||
|
||||
imageList.innerHTML = '';
|
||||
|
||||
state.files.forEach(file => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const li = document.createElement('li');
|
||||
li.className = "relative group cursor-move";
|
||||
li.dataset.fileName = file.name;
|
||||
li.innerHTML = `<img src="${url}" class="w-full h-full object-cover rounded-md border-2 border-gray-600"><p class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1">${file.name}</p>`;
|
||||
imageList.appendChild(li);
|
||||
});
|
||||
|
||||
Sortable.create(imageList);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function setupFileInputHandler(toolId: any) {
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const multiFileTools = ['merge', 'pdf-to-zip', 'jpg-to-pdf', 'png-to-pdf', 'webp-to-pdf', 'image-to-pdf', 'svg-to-pdf', 'bmp-to-pdf', 'heic-to-pdf', 'tiff-to-pdf'];
|
||||
const isMultiFileTool = multiFileTools.includes(toolId);
|
||||
let isFirstUpload = true;
|
||||
|
||||
const processFiles = async (newFiles: any) => {
|
||||
if (newFiles.length === 0) return;
|
||||
|
||||
if (!isMultiFileTool || isFirstUpload) {
|
||||
state.files = newFiles;
|
||||
} else {
|
||||
state.files = [...state.files, ...newFiles];
|
||||
}
|
||||
isFirstUpload = false;
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) {
|
||||
renderFileDisplay(fileDisplayArea, state.files);
|
||||
}
|
||||
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) {
|
||||
fileControls.classList.remove('hidden');
|
||||
|
||||
createIcons({icons});
|
||||
}
|
||||
|
||||
const singlePdfLoadTools = ['split', 'organize', 'rotate', 'add-page-numbers',
|
||||
'pdf-to-jpg', 'pdf-to-png', 'pdf-to-webp', 'compress', 'pdf-to-greyscale',
|
||||
'edit-metadata', 'remove-metadata', 'flatten', 'delete-pages', 'add-blank-page',
|
||||
'extract-pages', 'add-watermark', 'add-header-footer', 'invert-colors', 'view-metadata',
|
||||
'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', 'pdf-to-tiff', 'split-in-half',
|
||||
'page-dimensions', 'n-up', 'duplicate-organize', 'combine-single-page', 'fix-dimensions', 'change-background-color',
|
||||
'change-text-color', 'ocr-pdf', 'sign-pdf', 'remove-annotations', 'cropper', 'form-filler',
|
||||
];
|
||||
const simpleTools = ['encrypt', 'decrypt', 'change-permissions', 'pdf-to-markdown', 'word-to-pdf'];
|
||||
|
||||
if (isMultiFileTool) {
|
||||
handleMultiFileUpload(toolId);
|
||||
} else if (singlePdfLoadTools.includes(toolId)) {
|
||||
await handleSinglePdfUpload(toolId, state.files[0]);
|
||||
} else if (simpleTools.includes(toolId)) {
|
||||
const optionsDivId = toolId === 'change-permissions' ? 'permissions-options' : `${toolId}-options`;
|
||||
const optionsDiv = document.getElementById(optionsDivId);
|
||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
if (processBtn) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
processBtn.disabled = false;
|
||||
processBtn.onclick = () => {
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
func();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (toolId === 'edit') {
|
||||
const file = state.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
|
||||
const pdfContainer = document.getElementById('embed-pdf-container');
|
||||
|
||||
pdfContainer.innerHTML = '';
|
||||
|
||||
if (state.currentPdfUrl) {
|
||||
URL.revokeObjectURL(state.currentPdfUrl);
|
||||
}
|
||||
|
||||
pdfWrapper.classList.remove('hidden');
|
||||
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
|
||||
state.currentPdfUrl = fileURL;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.innerHTML = `
|
||||
import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js';
|
||||
EmbedPDF.init({
|
||||
type: 'container',
|
||||
target: document.getElementById('embed-pdf-container'),
|
||||
src: '${fileURL}',
|
||||
theme: 'dark',
|
||||
});
|
||||
`;
|
||||
document.head.appendChild(script);
|
||||
|
||||
const backBtn = document.getElementById('back-to-grid');
|
||||
const urlRevoker = () => {
|
||||
URL.revokeObjectURL(fileURL);
|
||||
state.currentPdfUrl = null; // Clear from state as well
|
||||
backBtn.removeEventListener('click', urlRevoker);
|
||||
};
|
||||
backBtn.addEventListener('click', urlRevoker);
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
fileInput.addEventListener('change', (e) => processFiles(Array.from(e.target.files)));
|
||||
|
||||
const setupAddMoreButton = () => {
|
||||
const addMoreBtn = document.getElementById('add-more-btn');
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', () => {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const setupClearButton = () => {
|
||||
const clearBtn = document.getElementById('clear-files-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
state.files = [];
|
||||
isFirstUpload = true;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fileInput.value = '';
|
||||
|
||||
const fileDisplayArea = document.getElementById('file-display-area');
|
||||
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||
|
||||
const fileControls = document.getElementById('file-controls');
|
||||
if (fileControls) fileControls.classList.add('hidden');
|
||||
|
||||
// Clear tool-specific UI
|
||||
const toolSpecificUI = ['file-list', 'page-merge-preview', 'image-list'];
|
||||
toolSpecificUI.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.innerHTML = '';
|
||||
});
|
||||
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
if (processBtn) processBtn.disabled = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setupAddMoreButton();
|
||||
setupClearButton();
|
||||
}, 100);
|
||||
}
|
||||
41
src/js/handlers/toolSelectionHandler.ts
Normal file
41
src/js/handlers/toolSelectionHandler.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { state } from '../state.js';
|
||||
import { dom, switchView, toolTemplates } from '../ui.js';
|
||||
import { setupFileInputHandler } from './fileHandler.js';
|
||||
import { toolLogic } from '../logic/index.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
const SETUP_AFTER_UPLOAD = ['sign-pdf'];
|
||||
|
||||
export function setupToolInterface(toolId: any) {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: 'instant' as ScrollBehavior
|
||||
});
|
||||
|
||||
state.activeTool = toolId;
|
||||
dom.toolContent.innerHTML = toolTemplates[toolId]();
|
||||
createIcons({icons});
|
||||
switchView('tool');
|
||||
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!fileInput && processBtn) {
|
||||
const logic = toolLogic[toolId];
|
||||
if (logic) {
|
||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||
processBtn.onclick = func;
|
||||
}
|
||||
}
|
||||
|
||||
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
||||
if (!SETUP_AFTER_UPLOAD.includes(toolId)) {
|
||||
toolLogic[toolId].setup();
|
||||
}
|
||||
}
|
||||
|
||||
if (fileInput) {
|
||||
setupFileInputHandler(toolId);
|
||||
}
|
||||
}
|
||||
50
src/js/logic/add-blank-page.ts
Normal file
50
src/js/logic/add-blank-page.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function addBlankPage() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageNumberInput = document.getElementById('page-number').value;
|
||||
if (pageNumberInput.trim() === '') {
|
||||
showAlert('Invalid Input', 'Please enter a page number.');
|
||||
return;
|
||||
}
|
||||
|
||||
const position = parseInt(pageNumberInput);
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
if (isNaN(position) || position < 0 || position > totalPages) {
|
||||
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Adding page...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
||||
const allIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||
|
||||
const indicesBefore = allIndices.slice(0, position);
|
||||
const indicesAfter = allIndices.slice(position);
|
||||
|
||||
if (indicesBefore.length > 0) {
|
||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesBefore);
|
||||
copied.forEach((p: any) => newPdf.addPage(p));
|
||||
}
|
||||
|
||||
newPdf.addPage([width, height]);
|
||||
|
||||
if (indicesAfter.length > 0) {
|
||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
||||
copied.forEach((p: any) => newPdf.addPage(p));
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'page-added.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not add a blank page.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
101
src/js/logic/add-header-footer.ts
Normal file
101
src/js/logic/add-header-footer.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, parsePageRanges } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||
|
||||
|
||||
export function setupHeaderFooterUI() {
|
||||
const totalPagesSpan = document.getElementById('total-pages');
|
||||
if (totalPagesSpan && state.pdfDoc) {
|
||||
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
|
||||
}
|
||||
}
|
||||
|
||||
export async function addHeaderFooter() {
|
||||
showLoader('Adding header & footer...');
|
||||
try {
|
||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const allPages = state.pdfDoc.getPages();
|
||||
const totalPages = allPages.length;
|
||||
const margin = 40;
|
||||
|
||||
// --- 1. Get new formatting options from the UI ---
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
|
||||
// @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('font-color').value;
|
||||
const fontColor = hexToRgb(colorHex);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageRangeInput = document.getElementById('page-range').value;
|
||||
|
||||
// --- 2. Get text values ---
|
||||
const texts = {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerLeft: document.getElementById('header-left').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerCenter: document.getElementById('header-center').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
headerRight: document.getElementById('header-right').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerLeft: document.getElementById('footer-left').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerCenter: document.getElementById('footer-center').value,
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
footerRight: document.getElementById('footer-right').value,
|
||||
};
|
||||
|
||||
// --- 3. Parse page range to determine which pages to modify ---
|
||||
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||
if (indicesToProcess.length === 0) {
|
||||
throw new Error("Invalid page range specified. Please check your input (e.g., '1-3, 5').");
|
||||
}
|
||||
|
||||
// --- 4. Define drawing options with new values ---
|
||||
const drawOptions = {
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
color: rgb(fontColor.r, fontColor.g, fontColor.b)
|
||||
};
|
||||
|
||||
// --- 5. Loop over only the selected pages ---
|
||||
for (const pageIndex of indicesToProcess) {
|
||||
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
||||
const page = allPages[pageIndex];
|
||||
const { width, height } = page.getSize();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
const pageNumber = pageIndex + 1; // For dynamic text
|
||||
|
||||
// Helper to replace placeholders like {page} and {total}
|
||||
const processText = (text: any) => text
|
||||
.replace(/{page}/g, pageNumber)
|
||||
.replace(/{total}/g, totalPages);
|
||||
|
||||
// Get processed text for the current page
|
||||
const processedTexts = {
|
||||
headerLeft: processText(texts.headerLeft),
|
||||
headerCenter: processText(texts.headerCenter),
|
||||
headerRight: processText(texts.headerRight),
|
||||
footerLeft: processText(texts.footerLeft),
|
||||
footerCenter: processText(texts.footerCenter),
|
||||
footerRight: processText(texts.footerRight),
|
||||
};
|
||||
|
||||
if (processedTexts.headerLeft) page.drawText(processedTexts.headerLeft, { ...drawOptions, x: margin, y: height - margin });
|
||||
if (processedTexts.headerCenter) page.drawText(processedTexts.headerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.headerCenter, fontSize) / 2, y: height - margin });
|
||||
if (processedTexts.headerRight) page.drawText(processedTexts.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.headerRight, fontSize), y: height - margin });
|
||||
if (processedTexts.footerLeft) page.drawText(processedTexts.footerLeft, { ...drawOptions, x: margin, y: margin });
|
||||
if (processedTexts.footerCenter) page.drawText(processedTexts.footerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.footerCenter, fontSize) / 2, y: margin });
|
||||
if (processedTexts.footerRight) page.drawText(processedTexts.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.footerRight, fontSize), y: margin });
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'header-footer-added.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not add header or footer.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
101
src/js/logic/add-page-numbers.ts
Normal file
101
src/js/logic/add-page-numbers.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { rgb, StandardFonts } from 'pdf-lib';
|
||||
|
||||
export async function addPageNumbers() {
|
||||
showLoader('Adding page numbers...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const position = document.getElementById('position').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const format = document.getElementById('number-format').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
|
||||
const pages = state.pdfDoc.getPages();
|
||||
const totalPages = pages.length;
|
||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const page = pages[i];
|
||||
|
||||
const mediaBox = page.getMediaBox();
|
||||
const cropBox = page.getCropBox();
|
||||
const bounds = cropBox || mediaBox;
|
||||
const width = bounds.width;
|
||||
const height = bounds.height;
|
||||
const xOffset = bounds.x || 0;
|
||||
const yOffset = bounds.y || 0;
|
||||
|
||||
let pageNumText = (format === 'page_x_of_y') ? `${i + 1} / ${totalPages}` : `${i + 1}`;
|
||||
|
||||
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
|
||||
const textHeight = fontSize;
|
||||
|
||||
const minMargin = 8;
|
||||
const maxMargin = 40;
|
||||
const marginPercentage = 0.04;
|
||||
|
||||
const horizontalMargin = Math.max(minMargin, Math.min(maxMargin, width * marginPercentage));
|
||||
const verticalMargin = Math.max(minMargin, Math.min(maxMargin, height * marginPercentage));
|
||||
|
||||
// Ensure text doesn't go outside visible page boundaries
|
||||
const safeHorizontalMargin = Math.max(horizontalMargin, textWidth / 2 + 3);
|
||||
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
||||
|
||||
let x, y;
|
||||
|
||||
switch (position) {
|
||||
case 'bottom-center':
|
||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
||||
y = safeVerticalMargin + yOffset;
|
||||
break;
|
||||
case 'top-center':
|
||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-left':
|
||||
x = safeHorizontalMargin + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
case 'top-right':
|
||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||
break;
|
||||
}
|
||||
|
||||
// Final safety check to ensure coordinates are within visible page bounds
|
||||
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
|
||||
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
|
||||
|
||||
page.drawText(pageNumText, {
|
||||
x, y,
|
||||
font: helveticaFont,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b)
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'paginated.pdf');
|
||||
showAlert('Success', 'Page numbers added successfully!');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not add page numbers.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
132
src/js/logic/add-watermark.ts
Normal file
132
src/js/logic/add-watermark.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, degrees, StandardFonts } from 'pdf-lib';
|
||||
|
||||
export function setupWatermarkUI() {
|
||||
const watermarkTypeRadios = document.querySelectorAll('input[name="watermark-type"]');
|
||||
const textOptions = document.getElementById('text-watermark-options');
|
||||
const imageOptions = document.getElementById('image-watermark-options');
|
||||
|
||||
watermarkTypeRadios.forEach(radio => {
|
||||
radio.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
|
||||
if (e.target.value === 'text') {
|
||||
textOptions.classList.remove('hidden');
|
||||
imageOptions.classList.add('hidden');
|
||||
} else {
|
||||
textOptions.classList.add('hidden');
|
||||
imageOptions.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const opacitySliderText = document.getElementById('opacity-text');
|
||||
const opacityValueText = document.getElementById('opacity-value-text');
|
||||
const angleSliderText = document.getElementById('angle-text');
|
||||
const angleValueText = document.getElementById('angle-value-text');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
opacitySliderText.addEventListener('input', () => opacityValueText.textContent = opacitySliderText.value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
angleSliderText.addEventListener('input', () => angleValueText.textContent = angleSliderText.value);
|
||||
|
||||
const opacitySliderImage = document.getElementById('opacity-image');
|
||||
const opacityValueImage = document.getElementById('opacity-value-image');
|
||||
const angleSliderImage = document.getElementById('angle-image');
|
||||
const angleValueImage = document.getElementById('angle-value-image');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
opacitySliderImage.addEventListener('input', () => opacityValueImage.textContent = opacitySliderImage.value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
angleSliderImage.addEventListener('input', () => angleValueImage.textContent = angleSliderImage.value);
|
||||
}
|
||||
|
||||
export async function addWatermark() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const watermarkType = document.querySelector('input[name="watermark-type"]:checked').value;
|
||||
|
||||
showLoader('Adding watermark...');
|
||||
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
let watermarkAsset = null;
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
} else { // 'image'
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const imageFile = document.getElementById('image-watermark-input').files[0];
|
||||
if (!imageFile) throw new Error('Please select an image file for the watermark.');
|
||||
|
||||
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
||||
if (imageFile.type === 'image/png') {
|
||||
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
|
||||
} else if (imageFile.type === 'image/jpeg') {
|
||||
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
|
||||
} else {
|
||||
throw new Error('Unsupported Image. Please use a PNG or JPG for the watermark.');
|
||||
}
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
const { width, height } = page.getSize();
|
||||
|
||||
if (watermarkType === 'text') {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const text = document.getElementById('watermark-text').value;
|
||||
if (!text.trim()) throw new Error('Please enter text for the watermark.');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 72;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const angle = parseInt(document.getElementById('angle-text').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
|
||||
const opacity = parseFloat(document.getElementById('opacity-text').value) || 0.3;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
||||
|
||||
page.drawText(text, {
|
||||
x: (width - textWidth) / 2,
|
||||
y: height / 2,
|
||||
font: watermarkAsset,
|
||||
size: fontSize,
|
||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const angle = parseInt(document.getElementById('angle-image').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
|
||||
const opacity = parseFloat(document.getElementById('opacity-image').value) || 0.3;
|
||||
|
||||
const scale = 0.5;
|
||||
const imgWidth = watermarkAsset.width * scale;
|
||||
const imgHeight = watermarkAsset.height * scale;
|
||||
|
||||
page.drawImage(watermarkAsset, {
|
||||
x: (width - imgWidth) / 2,
|
||||
y: (height - imgHeight) / 2,
|
||||
width: imgWidth,
|
||||
height: imgHeight,
|
||||
opacity: opacity,
|
||||
rotate: degrees(angle),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'watermarked.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not add the watermark. Please check your inputs.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
55
src/js/logic/bmp-to-pdf.ts
Normal file
55
src/js/logic/bmp-to-pdf.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
async function convertImageToPngBytes(file: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function bmpToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one BMP file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting BMP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'from_bmps.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert BMP to PDF. One of the files may be invalid.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
53
src/js/logic/change-background-color.ts
Normal file
53
src/js/logic/change-background-color.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
|
||||
export async function changeBackgroundColor() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
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('background-color').value;
|
||||
const color = hexToRgb(colorHex);
|
||||
|
||||
showLoader('Changing background color...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
|
||||
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
const { width, height } = originalPage.getSize();
|
||||
|
||||
const newPage = newPdfDoc.addPage([width, height]);
|
||||
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
color: rgb(color.r, color.g, color.b),
|
||||
});
|
||||
|
||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||
newPage.drawPage(embeddedPage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'background-changed.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not change the background color.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
121
src/js/logic/change-permissions.ts
Normal file
121
src/js/logic/change-permissions.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||
import blobStream from 'blob-stream';
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
|
||||
export async function changePermissions() {
|
||||
// --- 1. GATHER INPUTS FROM THE NEW UI ---
|
||||
const currentPassword = (document.getElementById('current-password') as HTMLInputElement).value;
|
||||
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement).value;
|
||||
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement).value;
|
||||
|
||||
// An owner password is required to enforce any permissions.
|
||||
if (!newOwnerPassword && (newUserPassword || document.querySelectorAll('input[type="checkbox"]:not(:checked)').length > 0)) {
|
||||
showAlert('Input Required', 'You must set a "New Owner Password" to enforce specific permissions or to set a user password.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Preparing to process...');
|
||||
|
||||
try {
|
||||
const file = state.files[0];
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
|
||||
// --- 2. UNLOCK PDF WITH CURRENT PASSWORD ---
|
||||
let pdf;
|
||||
try {
|
||||
pdf = await pdfjsLib.getDocument({
|
||||
data: pdfData as ArrayBuffer,
|
||||
password: currentPassword
|
||||
}).promise;
|
||||
} catch (e) {
|
||||
// This catch is specific to password errors in pdf.js
|
||||
if (e.name === 'PasswordException') {
|
||||
hideLoader();
|
||||
showAlert('Incorrect Password', 'The current password you entered is incorrect.');
|
||||
return;
|
||||
}
|
||||
throw e; // Re-throw other errors
|
||||
}
|
||||
|
||||
// --- 3. RASTERIZE PAGES (UNCHANGED LOGIC) ---
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent = 'Applying new permissions...';
|
||||
|
||||
// --- 4. GATHER ALL PERMISSION CHECKBOX VALUES ---
|
||||
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement).checked;
|
||||
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement).checked;
|
||||
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement).checked;
|
||||
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement).checked;
|
||||
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement).checked;
|
||||
const allowContentAccessibility = (document.getElementById('allow-content-accessibility') as HTMLInputElement).checked;
|
||||
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement).checked;
|
||||
|
||||
// --- 5. CREATE NEW PDF WITH PDFKIT USING ALL NEW SETTINGS ---
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
|
||||
|
||||
// Apply the new, separate user and owner passwords
|
||||
userPassword: newUserPassword,
|
||||
ownerPassword: newOwnerPassword,
|
||||
|
||||
// Apply all seven permissions from the checkboxes
|
||||
permissions: {
|
||||
printing: allowPrinting ? 'highResolution' : false,
|
||||
modifying: allowModifying,
|
||||
copying: allowCopying,
|
||||
annotating: allowAnnotating,
|
||||
fillingForms: allowFillingForms,
|
||||
contentAccessibility: allowContentAccessibility,
|
||||
documentAssembly: allowDocumentAssembly
|
||||
}
|
||||
});
|
||||
|
||||
const stream = doc.pipe(blobStream());
|
||||
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, {
|
||||
width: pageImages[i].width,
|
||||
height: pageImages[i].height
|
||||
});
|
||||
}
|
||||
|
||||
doc.end();
|
||||
|
||||
// --- 6. FINALIZE AND DOWNLOAD (UNCHANGED LOGIC) ---
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Permissions changed successfully!');
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
hideLoader();
|
||||
showAlert('Error', `An unexpected error occurred: ${e.message}`);
|
||||
}
|
||||
}
|
||||
144
src/js/logic/change-text-color.ts
Normal file
144
src/js/logic/change-text-color.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
let isRenderingPreview = false;
|
||||
let renderTimeout: any;
|
||||
|
||||
async function updateTextColorPreview() {
|
||||
if (isRenderingPreview) return;
|
||||
isRenderingPreview = true;
|
||||
|
||||
try {
|
||||
const textColorCanvas = document.getElementById('text-color-canvas');
|
||||
if (!textColorCanvas) return;
|
||||
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const page = await pdf.getPage(1); // Preview first page
|
||||
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');
|
||||
|
||||
// @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;
|
||||
// @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;
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const imageData = context.getImageData(0, 0, textColorCanvas.width, textColorCanvas.height);
|
||||
const data = imageData.data;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color-input').value;
|
||||
const { r, g, b } = hexToRgb(colorHex);
|
||||
const darknessThreshold = 120;
|
||||
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i] < darknessThreshold && data[i + 1] < darknessThreshold && data[i + 2] < darknessThreshold) {
|
||||
data[i] = r * 255;
|
||||
data[i + 1] = g * 255;
|
||||
data[i + 2] = b * 255;
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
} catch (error) {
|
||||
console.error('Error updating preview:', error);
|
||||
} finally {
|
||||
isRenderingPreview = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupTextColorTool() {
|
||||
const originalCanvas = document.getElementById('original-canvas');
|
||||
const colorInput = document.getElementById('text-color-input');
|
||||
|
||||
if (!originalCanvas || !colorInput) return;
|
||||
|
||||
// Debounce the preview update for performance
|
||||
colorInput.addEventListener('input', () => {
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(updateTextColorPreview, 250);
|
||||
});
|
||||
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const page = await pdf.getPage(1);
|
||||
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.width = viewport.width;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
||||
originalCanvas.height = viewport.height;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
||||
await page.render({ canvasContext: originalCanvas.getContext('2d'), viewport }).promise;
|
||||
await updateTextColorPreview();
|
||||
}
|
||||
|
||||
export async function changeTextColor() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
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').value;
|
||||
const { r, g, b } = hexToRgb(colorHex);
|
||||
const darknessThreshold = 120;
|
||||
|
||||
showLoader('Changing text color...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 }); // High resolution for quality
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) {
|
||||
data[j] = r * 255;
|
||||
data[j + 1] = g * 255;
|
||||
data[j + 2] = b * 255;
|
||||
}
|
||||
}
|
||||
context.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png'));
|
||||
|
||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'text-color-changed.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not change text color.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
77
src/js/logic/combine-single-page.ts
Normal file
77
src/js/logic/combine-single-page.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
|
||||
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 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
|
||||
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
|
||||
const addSeparator = document.getElementById('add-separator').checked;
|
||||
const backgroundColor = hexToRgb(backgroundColorHex);
|
||||
|
||||
showLoader('Combining pages...');
|
||||
try {
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
|
||||
let maxWidth = 0;
|
||||
let totalHeight = 0;
|
||||
sourcePages.forEach((page: any) => {
|
||||
const { width, height } = page.getSize();
|
||||
if (width > maxWidth) maxWidth = width;
|
||||
totalHeight += height;
|
||||
});
|
||||
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
|
||||
|
||||
const newPage = newDoc.addPage([maxWidth, totalHeight]);
|
||||
|
||||
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
|
||||
newPage.drawRectangle({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: maxWidth,
|
||||
height: totalHeight,
|
||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||
});
|
||||
}
|
||||
|
||||
let currentY = totalHeight;
|
||||
for (let i = 0; i < sourcePages.length; i++) {
|
||||
const sourcePage = sourcePages[i];
|
||||
const { width, height } = sourcePage.getSize();
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
currentY -= height;
|
||||
const x = (maxWidth - width) / 2;
|
||||
|
||||
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
||||
|
||||
if (addSeparator && i < sourcePages.length - 1) {
|
||||
const lineY = currentY - (spacing / 2);
|
||||
newPage.drawLine({
|
||||
start: { x: 0, y: lineY },
|
||||
end: { x: maxWidth, y: lineY },
|
||||
thickness: 0.5,
|
||||
color: rgb(0.8, 0.8, 0.8),
|
||||
});
|
||||
}
|
||||
|
||||
currentY -= spacing;
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'combined-page.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while combining pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
193
src/js/logic/compare-pdfs.ts
Normal file
193
src/js/logic/compare-pdfs.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { icons, createIcons } from "lucide";
|
||||
|
||||
const state = {
|
||||
pdfDoc1: null,
|
||||
pdfDoc2: null,
|
||||
currentPage: 1,
|
||||
viewMode: 'overlay',
|
||||
isSyncScroll: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a PDF page to fit the width of its container.
|
||||
* @param {PDFDocumentProxy} pdfDoc - The loaded PDF document from pdf.js.
|
||||
* @param {number} pageNum - The page number to render.
|
||||
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
|
||||
* @param {HTMLElement} container - The container to fit the canvas into.
|
||||
*/
|
||||
async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any) {
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
|
||||
// Calculate scale to fit the container width.
|
||||
const containerWidth = container.clientWidth - 2; // Subtract border width
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
const scale = containerWidth / viewport.width;
|
||||
const scaledViewport = page.getViewport({ scale: scale });
|
||||
|
||||
canvas.width = scaledViewport.width;
|
||||
canvas.height = scaledViewport.height;
|
||||
|
||||
await page.render({
|
||||
canvasContext: canvas.getContext('2d'),
|
||||
viewport: scaledViewport,
|
||||
}).promise;
|
||||
}
|
||||
|
||||
|
||||
async function renderBothPages() {
|
||||
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
||||
|
||||
showLoader(`Loading page ${state.currentPage}...`);
|
||||
|
||||
const canvas1 = document.getElementById('canvas-compare-1');
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
const panel1 = document.getElementById('panel-1');
|
||||
const panel2 = document.getElementById('panel-2');
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||
|
||||
// Determine the correct container based on the view mode
|
||||
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
|
||||
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
|
||||
|
||||
await Promise.all([
|
||||
renderPage(state.pdfDoc1, Math.min(state.currentPage, state.pdfDoc1.numPages), canvas1, container1),
|
||||
renderPage(state.pdfDoc2, Math.min(state.currentPage, state.pdfDoc2.numPages), canvas2, container2)
|
||||
]);
|
||||
|
||||
updateNavControls();
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
function updateNavControls() {
|
||||
const maxPages = Math.max(state.pdfDoc1?.numPages || 0, state.pdfDoc2?.numPages || 0);
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
document.getElementById('current-page-display-compare').textContent = state.currentPage;
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
document.getElementById('total-pages-display-compare').textContent = maxPages;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page-compare').disabled = state.currentPage <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page-compare').disabled = state.currentPage >= maxPages;
|
||||
}
|
||||
|
||||
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
||||
const fileInput = document.getElementById(inputId);
|
||||
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
|
||||
|
||||
const handleFile = async (file: any) => {
|
||||
if (!file || file.type !== 'application/pdf') return showAlert('Invalid File', 'Please select a valid PDF file.');
|
||||
|
||||
const displayDiv = document.getElementById(displayId);
|
||||
displayDiv.innerHTML = `<i data-lucide="check-circle" class="w-10 h-10 mb-3 text-green-500"></i><p class="text-sm text-gray-300 truncate">${file.name}</p>`;
|
||||
createIcons({icons});
|
||||
|
||||
try {
|
||||
showLoader(`Loading ${file.name}...`);
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
|
||||
|
||||
if (state.pdfDoc1 && state.pdfDoc2) {
|
||||
document.getElementById('compare-viewer').classList.remove('hidden');
|
||||
state.currentPage = 1;
|
||||
await renderBothPages();
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('Error', 'Could not load PDF. It may be corrupt or password-protected.');
|
||||
console.error(e);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
|
||||
dropZone.addEventListener('dragover', (e) => e.preventDefault());
|
||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the UI between Overlay and Side-by-Side views.
|
||||
* @param {'overlay' | 'side-by-side'} mode
|
||||
*/
|
||||
function setViewMode(mode: any) {
|
||||
state.viewMode = mode;
|
||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||
const overlayControls = document.getElementById('overlay-controls');
|
||||
const sideControls = document.getElementById('side-by-side-controls');
|
||||
const btnOverlay = document.getElementById('view-mode-overlay');
|
||||
const btnSide = document.getElementById('view-mode-side');
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
const opacitySlider = document.getElementById('opacity-slider');
|
||||
|
||||
|
||||
if (mode === 'overlay') {
|
||||
wrapper.className = 'compare-viewer-wrapper overlay-mode';
|
||||
overlayControls.classList.remove('hidden');
|
||||
sideControls.classList.add('hidden');
|
||||
btnOverlay.classList.add('bg-indigo-600');
|
||||
btnSide.classList.remove('bg-indigo-600');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
canvas2.style.opacity = opacitySlider.value;
|
||||
} else {
|
||||
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
|
||||
overlayControls.classList.add('hidden');
|
||||
sideControls.classList.remove('hidden');
|
||||
btnOverlay.classList.remove('bg-indigo-600');
|
||||
btnSide.classList.add('bg-indigo-600');
|
||||
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
|
||||
canvas2.style.opacity = '1';
|
||||
}
|
||||
renderBothPages();
|
||||
}
|
||||
|
||||
export function setupCompareTool() {
|
||||
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
|
||||
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
|
||||
|
||||
document.getElementById('prev-page-compare').addEventListener('click', () => {
|
||||
if (state.currentPage > 1) { state.currentPage--; renderBothPages(); }
|
||||
});
|
||||
document.getElementById('next-page-compare').addEventListener('click', () => {
|
||||
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
||||
if (state.currentPage < maxPages) { state.currentPage++; renderBothPages(); }
|
||||
});
|
||||
|
||||
document.getElementById('view-mode-overlay').addEventListener('click', () => setViewMode('overlay'));
|
||||
document.getElementById('view-mode-side').addEventListener('click', () => setViewMode('side-by-side'));
|
||||
|
||||
const canvas2 = document.getElementById('canvas-compare-2');
|
||||
document.getElementById('flicker-btn').addEventListener('click', () => {
|
||||
canvas2.style.transition = 'opacity 150ms ease-in-out';
|
||||
canvas2.style.opacity = (canvas2.style.opacity === '0') ? '1' : '0';
|
||||
});
|
||||
document.getElementById('opacity-slider').addEventListener('input', (e) => {
|
||||
canvas2.style.transition = '';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
canvas2.style.opacity = e.target.value;
|
||||
});
|
||||
|
||||
const panel1 = document.getElementById('panel-1');
|
||||
const panel2 = document.getElementById('panel-2');
|
||||
const syncToggle = document.getElementById('sync-scroll-toggle');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
syncToggle.addEventListener('change', () => { state.isSyncScroll = syncToggle.checked; });
|
||||
|
||||
let scrollingPanel: any = null;
|
||||
panel1.addEventListener('scroll', () => {
|
||||
if (state.isSyncScroll && scrollingPanel !== panel2) {
|
||||
scrollingPanel = panel1;
|
||||
panel2.scrollTop = panel1.scrollTop;
|
||||
setTimeout(() => scrollingPanel = null, 100);
|
||||
}
|
||||
});
|
||||
panel2.addEventListener('scroll', () => {
|
||||
if (state.isSyncScroll && scrollingPanel !== panel1) {
|
||||
scrollingPanel = panel2;
|
||||
panel1.scrollTop = panel2.scrollTop;
|
||||
setTimeout(() => scrollingPanel = null, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
279
src/js/logic/compress.ts
Normal file
279
src/js/logic/compress.ts
Normal file
@@ -0,0 +1,279 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
|
||||
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
|
||||
|
||||
function dataUrlToBytes(dataUrl: any) {
|
||||
const base64 = dataUrl.split(',')[1];
|
||||
const binaryString = atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
async function performSmartCompression(arrayBuffer: any, settings: any) {
|
||||
const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
|
||||
const pages = pdfDoc.getPages();
|
||||
|
||||
if (settings.removeMetadata) {
|
||||
try {
|
||||
pdfDoc.setTitle('');
|
||||
pdfDoc.setAuthor('');
|
||||
pdfDoc.setSubject('');
|
||||
pdfDoc.setKeywords([]);
|
||||
pdfDoc.setCreator('');
|
||||
pdfDoc.setProducer('');
|
||||
} catch (e) {
|
||||
console.warn('Could not remove metadata:', e);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i];
|
||||
const resources = page.node.Resources();
|
||||
if (!resources) continue;
|
||||
|
||||
const xobjects = resources.lookup(PDFName.of('XObject'));
|
||||
if (!(xobjects instanceof PDFDict)) continue;
|
||||
|
||||
for (const [key, value] of xobjects.entries()) {
|
||||
const stream = pdfDoc.context.lookup(value);
|
||||
if (!(stream instanceof PDFStream) || stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')) continue;
|
||||
|
||||
try {
|
||||
const imageBytes = stream.getContents();
|
||||
if (imageBytes.length < settings.skipSize) continue;
|
||||
|
||||
const width = stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
|
||||
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
|
||||
: 0;
|
||||
const height = stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
|
||||
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
|
||||
: 0;
|
||||
const bitsPerComponent = stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
||||
? (stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber).asNumber()
|
||||
: 8;
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
let newWidth = width;
|
||||
let newHeight = height;
|
||||
|
||||
const scaleFactor = settings.scaleFactor || 1.0;
|
||||
newWidth = Math.floor(width * scaleFactor);
|
||||
newHeight = Math.floor(height * scaleFactor);
|
||||
|
||||
if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) {
|
||||
const aspectRatio = newWidth / newHeight;
|
||||
if (newWidth > newHeight) {
|
||||
newWidth = Math.min(newWidth, settings.maxWidth);
|
||||
newHeight = newWidth / aspectRatio;
|
||||
} else {
|
||||
newHeight = Math.min(newHeight, settings.maxHeight);
|
||||
newWidth = newHeight * aspectRatio;
|
||||
}
|
||||
}
|
||||
|
||||
const minDim = settings.minDimension || 50;
|
||||
if (newWidth < minDim || newHeight < minDim) continue;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = Math.floor(newWidth);
|
||||
canvas.height = Math.floor(newHeight);
|
||||
|
||||
const img = new Image();
|
||||
const imageUrl = URL.createObjectURL(new Blob([imageBytes]));
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
img.src = imageUrl;
|
||||
});
|
||||
|
||||
ctx.imageSmoothingEnabled = settings.smoothing !== false;
|
||||
ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium';
|
||||
|
||||
if (settings.grayscale) {
|
||||
ctx.filter = 'grayscale(100%)';
|
||||
} else if (settings.contrast) {
|
||||
ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`;
|
||||
}
|
||||
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
let bestBytes = null;
|
||||
let bestSize = imageBytes.length;
|
||||
|
||||
const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality);
|
||||
const jpegBytes = dataUrlToBytes(jpegDataUrl);
|
||||
if (jpegBytes.length < bestSize) {
|
||||
bestBytes = jpegBytes;
|
||||
bestSize = jpegBytes.length;
|
||||
}
|
||||
|
||||
if (settings.tryWebP) {
|
||||
try {
|
||||
const webpDataUrl = canvas.toDataURL('image/webp', settings.quality);
|
||||
const webpBytes = dataUrlToBytes(webpDataUrl);
|
||||
if (webpBytes.length < bestSize) {
|
||||
bestBytes = webpBytes;
|
||||
bestSize = webpBytes.length;
|
||||
}
|
||||
} catch (e) { /* WebP not supported */ }
|
||||
}
|
||||
|
||||
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
|
||||
(stream as any).contents = bestBytes;
|
||||
stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize));
|
||||
stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width));
|
||||
stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height));
|
||||
stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode'));
|
||||
stream.dict.delete(PDFName.of('DecodeParms'));
|
||||
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
|
||||
|
||||
if (settings.grayscale) {
|
||||
stream.dict.set(PDFName.of('ColorSpace'), PDFName.of('DeviceGray'));
|
||||
}
|
||||
}
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Skipping an uncompressible image in smart mode:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveOptions = {
|
||||
useObjectStreams: settings.useObjectStreams !== false,
|
||||
addDefaultPage: false,
|
||||
objectsPerTick: settings.objectsPerTick || 50
|
||||
};
|
||||
|
||||
return await pdfDoc.save(saveOptions);
|
||||
}
|
||||
|
||||
async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
|
||||
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
|
||||
const page = await pdfJsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: settings.scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({ canvasContext: context, viewport, canvas: canvas }).promise;
|
||||
|
||||
const jpegBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', settings.quality));
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const jpegBytes = await jpegBlob.arrayBuffer();
|
||||
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
newPage.drawImage(jpegImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
||||
}
|
||||
return await newPdfDoc.save();
|
||||
}
|
||||
|
||||
export async function compress() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const level = document.getElementById('compression-level').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const algorithm = document.getElementById('compression-algorithm').value;
|
||||
|
||||
const settings = {
|
||||
'balanced': {
|
||||
smart: { quality: 0.5, threshold: 0.95, maxWidth: 1800, maxHeight: 1800, skipSize: 3000 },
|
||||
legacy: { scale: 1.5, quality: 0.6 }
|
||||
},
|
||||
'high-quality': {
|
||||
smart: { quality: 0.70, threshold: 0.98, maxWidth: 2500, maxHeight: 2500, skipSize: 5000 },
|
||||
legacy: { scale: 2.0, quality: 0.9 }
|
||||
},
|
||||
'small-size': {
|
||||
smart: { quality: 0.3, threshold: 0.95, maxWidth: 1200, maxHeight: 1200, skipSize: 2000 },
|
||||
legacy: { scale: 1.2, quality: 0.4 }
|
||||
},
|
||||
'extreme': {
|
||||
smart: { quality: 0.1, threshold: 0.95, maxWidth: 1000, maxHeight: 1000, skipSize: 1000 },
|
||||
legacy: { scale: 1.0, quality: 0.2 }
|
||||
}
|
||||
};
|
||||
|
||||
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
||||
const legacySettings = settings[level].legacy;
|
||||
|
||||
try {
|
||||
const originalFile = state.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
|
||||
|
||||
let resultBytes;
|
||||
let usedMethod;
|
||||
|
||||
if (algorithm === 'vector') {
|
||||
showLoader('Running Vector (Smart) compression...');
|
||||
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
||||
usedMethod = 'Vector';
|
||||
} else if (algorithm === 'photon') {
|
||||
showLoader('Running Photon (Rasterize) compression...');
|
||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
||||
usedMethod = 'Photon';
|
||||
} else {
|
||||
showLoader('Running Automatic (Vector first)...');
|
||||
const vectorResultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
||||
|
||||
if (vectorResultBytes.length < originalFile.size) {
|
||||
resultBytes = vectorResultBytes;
|
||||
usedMethod = 'Vector (Automatic)';
|
||||
} else {
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||
showAlert('Vector failed to reduce size. Trying Photon...', 'info', 3000);
|
||||
showLoader('Running Automatic (Photon fallback)...');
|
||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
||||
usedMethod = 'Photon (Automatic)';
|
||||
}
|
||||
}
|
||||
|
||||
const originalSize = formatBytes(originalFile.size);
|
||||
const compressedSize = formatBytes(resultBytes.length);
|
||||
const savings = originalFile.size - resultBytes.length;
|
||||
const savingsPercent = savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
||||
|
||||
if (savings > 0) {
|
||||
showAlert(
|
||||
'Compression Complete',
|
||||
`Method: **${usedMethod}**. ` +
|
||||
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
|
||||
);
|
||||
} else {
|
||||
showAlert(
|
||||
'Compression Finished',
|
||||
`Method: **${usedMethod}**. ` +
|
||||
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
||||
downloadFile(new Blob([resultBytes], { type: 'application/pdf' }), 'compressed-final.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||
showAlert('Error', `An error occurred during compression. Error: ${e.message}`, 'error');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
320
src/js/logic/cropper.ts
Normal file
320
src/js/logic/cropper.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import Cropper from "cropperjs";
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
|
||||
// --- Global State for the Cropper Tool ---
|
||||
const cropperState = {
|
||||
pdfDoc: null,
|
||||
currentPageNum: 1,
|
||||
cropper: null,
|
||||
originalPdfBytes: null,
|
||||
cropperImageElement: null,
|
||||
pageCrops: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the current crop data to the state object.
|
||||
*/
|
||||
function saveCurrentCrop() {
|
||||
if (cropperState.cropper) {
|
||||
const currentCrop = cropperState.cropper.getData(true);
|
||||
const imageData = cropperState.cropper.getImageData();
|
||||
const cropPercentages = {
|
||||
x: currentCrop.x / imageData.naturalWidth,
|
||||
y: currentCrop.y / imageData.naturalHeight,
|
||||
width: currentCrop.width / imageData.naturalWidth,
|
||||
height: currentCrop.height / imageData.naturalHeight,
|
||||
};
|
||||
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a PDF page to the Cropper UI as an image.
|
||||
* @param {number} num The page number to render.
|
||||
*/
|
||||
async function displayPageAsImage(num: any) {
|
||||
showLoader(`Rendering Page ${num}...`);
|
||||
|
||||
try {
|
||||
const page = await cropperState.pdfDoc.getPage(num);
|
||||
const viewport = page.getViewport({ scale: 2.5 });
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||
|
||||
if (cropperState.cropper) {
|
||||
cropperState.cropper.destroy();
|
||||
}
|
||||
|
||||
const image = document.createElement('img');
|
||||
image.src = tempCanvas.toDataURL('image/png');
|
||||
document.getElementById('cropper-container').innerHTML = '';
|
||||
document.getElementById('cropper-container').appendChild(image);
|
||||
|
||||
image.onload = () => {
|
||||
cropperState.cropper = new Cropper(image, {
|
||||
viewMode: 1,
|
||||
background: false,
|
||||
autoCropArea: 0.8,
|
||||
responsive: true,
|
||||
rotatable: false,
|
||||
zoomable: false,
|
||||
});
|
||||
|
||||
// Restore saved crop data for this page
|
||||
const savedCrop = cropperState.pageCrops[num];
|
||||
if (savedCrop) {
|
||||
const imageData = cropperState.cropper.getImageData();
|
||||
const cropData = {
|
||||
x: savedCrop.x * imageData.naturalWidth,
|
||||
y: savedCrop.y * imageData.naturalHeight,
|
||||
width: savedCrop.width * imageData.naturalWidth,
|
||||
height: savedCrop.height * imageData.naturalHeight,
|
||||
};
|
||||
cropperState.cropper.setData(cropData);
|
||||
}
|
||||
|
||||
updatePageInfo();
|
||||
enableControls();
|
||||
hideLoader();
|
||||
showAlert('Ready', 'Please select an area to crop.');
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error rendering page:", error);
|
||||
showAlert('Error', 'Failed to render page.');
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles page navigation.
|
||||
* @param {number} offset -1 for previous, 1 for next.
|
||||
*/
|
||||
async function changePage(offset: any) {
|
||||
// Save the current page's crop before changing
|
||||
saveCurrentCrop();
|
||||
|
||||
const newPageNum = cropperState.currentPageNum + offset;
|
||||
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
|
||||
cropperState.currentPageNum = newPageNum;
|
||||
await displayPageAsImage(cropperState.currentPageNum);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePageInfo() {
|
||||
document.getElementById('page-info').textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
||||
}
|
||||
|
||||
function enableControls() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('prev-page').disabled = cropperState.currentPageNum <= 1;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('next-page').disabled = cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('crop-button').disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a non-destructive crop by updating the page's crop box.
|
||||
*/
|
||||
async function performMetadataCrop(pdfToModify: any, cropData: any) {
|
||||
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 page = pdfToModify.getPages()[pageNum - 1];
|
||||
const { width: pageWidth, height: pageHeight } = page.getSize();
|
||||
const rotation = page.getRotation().angle;
|
||||
const crop = cropData[pageNum];
|
||||
|
||||
const visualPdfWidth = pageWidth * crop.width;
|
||||
const visualPdfHeight = pageHeight * crop.height;
|
||||
const visualPdfX = pageWidth * crop.x;
|
||||
const visualPdfY = pageHeight * crop.y;
|
||||
|
||||
let finalX, finalY, finalWidth, finalHeight;
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
finalX = visualPdfY;
|
||||
finalY = pageWidth - visualPdfX - visualPdfWidth;
|
||||
finalWidth = visualPdfHeight;
|
||||
finalHeight = visualPdfWidth;
|
||||
break;
|
||||
case 180:
|
||||
finalX = pageWidth - visualPdfX - visualPdfWidth;
|
||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
||||
finalWidth = visualPdfWidth;
|
||||
finalHeight = visualPdfHeight;
|
||||
break;
|
||||
case 270:
|
||||
finalX = pageHeight - visualPdfY - visualPdfHeight;
|
||||
finalY = visualPdfX;
|
||||
finalWidth = visualPdfHeight;
|
||||
finalHeight = visualPdfWidth;
|
||||
break;
|
||||
default:
|
||||
finalX = visualPdfX;
|
||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
||||
finalWidth = visualPdfWidth;
|
||||
finalHeight = visualPdfHeight;
|
||||
break;
|
||||
}
|
||||
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
|
||||
}
|
||||
}
|
||||
|
||||
// FILE: js/logic/cropper.js
|
||||
|
||||
/**
|
||||
* Performs a destructive crop by flattening the selected area to an image.
|
||||
*/
|
||||
async function performFlatteningCrop(cropData: any) {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
||||
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes);
|
||||
|
||||
// CORRECTED: Use .numPages from the pdf.js document object
|
||||
const totalPages = cropperState.pdfDoc.numPages;
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
const pageNum = i + 1;
|
||||
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
|
||||
|
||||
if (cropData[pageNum]) {
|
||||
const page = await cropperState.pdfDoc.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 2.5 });
|
||||
|
||||
const tempCanvas = document.createElement('canvas');
|
||||
const tempCtx = tempCanvas.getContext('2d');
|
||||
tempCanvas.width = viewport.width;
|
||||
tempCanvas.height = viewport.height;
|
||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||
|
||||
const finalCanvas = document.createElement('canvas');
|
||||
const finalCtx = finalCanvas.getContext('2d');
|
||||
const crop = cropData[pageNum];
|
||||
const finalWidth = tempCanvas.width * crop.width;
|
||||
const finalHeight = tempCanvas.height * crop.height;
|
||||
finalCanvas.width = finalWidth;
|
||||
finalCanvas.height = finalHeight;
|
||||
|
||||
finalCtx.drawImage(
|
||||
tempCanvas,
|
||||
tempCanvas.width * crop.x, tempCanvas.height * crop.y,
|
||||
finalWidth, finalHeight,
|
||||
0, 0, finalWidth, finalHeight
|
||||
);
|
||||
|
||||
const pngBytes = await new Promise(res => finalCanvas.toBlob(blob => blob.arrayBuffer().then(res), 'image/png'));
|
||||
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
||||
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight });
|
||||
} else {
|
||||
// Correctly copy the page from the source pdf-lib document
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
return newPdfDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to set up the Cropper tool.
|
||||
*/
|
||||
export async function setupCropperTool() {
|
||||
if (state.files.length === 0) return;
|
||||
|
||||
// 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 = `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js`;
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
try {
|
||||
// const loadingTask = pdfjsLib.getDocument({ data: cropperState.originalPdfBytes });
|
||||
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
|
||||
|
||||
cropperState.pdfDoc = await loadingTask.promise;
|
||||
cropperState.currentPageNum = 1;
|
||||
|
||||
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();
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const isDestructive = document.getElementById('destructive-crop-toggle').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const isApplyToAll = document.getElementById('apply-to-all-toggle').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) {
|
||||
console.error("Error setting up cropper tool:", error);
|
||||
showAlert('Error', 'Failed to load PDF for cropping.');
|
||||
}
|
||||
}
|
||||
65
src/js/logic/decrypt.ts
Normal file
65
src/js/logic/decrypt.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||
import blobStream from 'blob-stream';
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
|
||||
export async function decrypt() {
|
||||
const file = state.files[0];
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const password = document.getElementById('password-input').value;
|
||||
if (!password.trim()) {
|
||||
showAlert('Input Required', 'Please enter the PDF password.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoader('Preparing to process...');
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData, password: password }).promise;
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent = 'Building unlocked PDF...';
|
||||
const doc = new PDFDocument({ size: [pageImages[0].width, pageImages[0].height] });
|
||||
const stream = doc.pipe(blobStream());
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
|
||||
}
|
||||
doc.end();
|
||||
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `unlocked-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Decryption complete! Your download has started.');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error during PDF decryption:", error);
|
||||
hideLoader();
|
||||
if (error.name === 'PasswordException') {
|
||||
showAlert('Incorrect Password', 'The password you entered is incorrect.');
|
||||
} else {
|
||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/js/logic/delete-pages.ts
Normal file
57
src/js/logic/delete-pages.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function deletePages() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageInput = document.getElementById('pages-to-delete').value;
|
||||
if (!pageInput) {
|
||||
showAlert('Invalid Input', 'Please enter page numbers to delete.');
|
||||
return;
|
||||
}
|
||||
showLoader('Deleting pages...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
const indicesToDelete = new Set();
|
||||
const ranges = pageInput.split(',');
|
||||
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToDelete.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (indicesToDelete.size === 0) {
|
||||
showAlert('Invalid Input', 'No valid pages selected for deletion.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
if (indicesToDelete.size >= totalPages) {
|
||||
showAlert('Invalid Input', 'You cannot delete all pages.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const indicesToKeep = Array.from({ length: totalPages }, (_, i) => i).filter(index => !indicesToDelete.has(index));
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'deleted-pages.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not delete pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
145
src/js/logic/duplicate-organize.ts
Normal file
145
src/js/logic/duplicate-organize.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import Sortable from 'sortablejs'
|
||||
import {icons, createIcons} from "lucide";
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
const duplicateOrganizeState = {
|
||||
sortableInstances: {}
|
||||
};
|
||||
|
||||
function initializePageGridSortable() {
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (!grid) return;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||
if (duplicateOrganizeState.sortableInstances.pageGrid) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||
duplicateOrganizeState.sortableInstances.pageGrid.destroy();
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||
duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
filter: '.duplicate-btn, .delete-btn',
|
||||
preventOnFilter: true,
|
||||
onStart: function(evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function(evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches event listeners for duplicate and delete to a page thumbnail element.
|
||||
* @param {HTMLElement} element The thumbnail element to attach listeners to.
|
||||
*/
|
||||
function attachEventListeners(element: any) {
|
||||
// Re-number all visible page labels
|
||||
const renumberPages = () => {
|
||||
const grid = document.getElementById('page-grid');
|
||||
const pages = grid.querySelectorAll('.page-number');
|
||||
pages.forEach((label, index) => {
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||
label.textContent = index + 1;
|
||||
});
|
||||
};
|
||||
|
||||
// Duplicate button listener
|
||||
element.querySelector('.duplicate-btn').addEventListener('click', (e: any) => {
|
||||
e.stopPropagation();
|
||||
const clone = element.cloneNode(true);
|
||||
element.after(clone);
|
||||
attachEventListeners(clone);
|
||||
renumberPages();
|
||||
initializePageGridSortable();
|
||||
});
|
||||
|
||||
element.querySelector('.delete-btn').addEventListener('click', (e: any) => {
|
||||
e.stopPropagation();
|
||||
if (document.getElementById('page-grid').children.length > 1) {
|
||||
element.remove();
|
||||
renumberPages();
|
||||
initializePageGridSortable();
|
||||
} else {
|
||||
showAlert('Cannot Delete', 'You cannot delete the last page of the document.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function renderDuplicateOrganizeThumbnails() {
|
||||
const grid = document.getElementById('page-grid');
|
||||
if (!grid) return;
|
||||
|
||||
showLoader('Rendering page previews...');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
grid.innerHTML = '';
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = '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'.
|
||||
wrapper.dataset.originalPageIndex = i - 1;
|
||||
|
||||
wrapper.innerHTML = `
|
||||
<div class="w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600">
|
||||
<img src="${canvas.toDataURL()}" class="max-w-full max-h-full object-contain">
|
||||
</div>
|
||||
<span class="page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1">${i}</span>
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<button class="duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center" title="Duplicate Page">
|
||||
<i data-lucide="copy-plus" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<button class="delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center" title="Delete Page">
|
||||
<i data-lucide="x-circle" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(wrapper);
|
||||
attachEventListeners(wrapper);
|
||||
}
|
||||
|
||||
initializePageGridSortable();
|
||||
createIcons({icons});
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
|
||||
export async function processAndSave() {
|
||||
showLoader('Building new PDF...');
|
||||
try {
|
||||
const grid = document.getElementById('page-grid');
|
||||
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const finalIndices = Array.from(finalPageElements).map(el => parseInt(el.dataset.originalPageIndex));
|
||||
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'organized.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to save the new PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
73
src/js/logic/edit-metadata.ts
Normal file
73
src/js/logic/edit-metadata.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFName, PDFString } from "pdf-lib"
|
||||
|
||||
|
||||
export async function editMetadata() {
|
||||
showLoader('Updating metadata...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const keywords = document.getElementById('meta-keywords').value;
|
||||
state.pdfDoc.setKeywords(keywords.split(',').map((k: any) => k.trim()).filter(Boolean));
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const creationDate = document.getElementById('meta-creation-date').value;
|
||||
if (creationDate) {
|
||||
state.pdfDoc.setCreationDate(new Date(creationDate));
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const modDate = document.getElementById('meta-mod-date').value;
|
||||
if (modDate) {
|
||||
state.pdfDoc.setModificationDate(new Date(modDate));
|
||||
} else {
|
||||
state.pdfDoc.setModificationDate(new Date());
|
||||
}
|
||||
|
||||
const infoDict = state.pdfDoc.getInfoDict();
|
||||
const standardKeys = new Set(['Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', 'CreationDate', 'ModDate']);
|
||||
|
||||
const allKeys = infoDict.keys().map((key: any) => key.asString().substring(1)); // Clean keys
|
||||
|
||||
allKeys.forEach((key: any) => {
|
||||
if (!standardKeys.has(key)) {
|
||||
infoDict.delete(PDFName.of(key));
|
||||
}
|
||||
});
|
||||
|
||||
const customKeys = document.querySelectorAll('.custom-meta-key');
|
||||
const customValues = document.querySelectorAll('.custom-meta-value');
|
||||
|
||||
customKeys.forEach((keyInput, index) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const key = keyInput.value.trim();
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const value = customValues[index].value.trim();
|
||||
if (key && value) {
|
||||
// Now we add the fields to a clean slate
|
||||
infoDict.set(PDFName.of(key), PDFString.of(value));
|
||||
}
|
||||
});
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-edited.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
73
src/js/logic/encrypt.ts
Normal file
73
src/js/logic/encrypt.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||
import blobStream from 'blob-stream';
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
|
||||
export async function encrypt() {
|
||||
const file = state.files[0];
|
||||
const password = (document.getElementById('password-input') as HTMLInputElement).value;
|
||||
if (!password.trim()) {
|
||||
showAlert('Input Required', 'Please enter a password.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showLoader('Preparing to process...');
|
||||
const pdfData = await readFileAsArrayBuffer(file);
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer }).promise;
|
||||
const numPages = pdf.numPages;
|
||||
const pageImages = [];
|
||||
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
||||
pageImages.push({
|
||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||
width: viewport.width,
|
||||
height: viewport.height
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('loader-text').textContent = 'Encrypting and building PDF...';
|
||||
const doc = new PDFDocument({
|
||||
size: [pageImages[0].width, pageImages[0].height],
|
||||
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
|
||||
userPassword: password,
|
||||
ownerPassword: password,
|
||||
permissions: {
|
||||
printing: 'highResolution',
|
||||
modifying: false,
|
||||
copying: false,
|
||||
annotating: false,
|
||||
fillingForms: false,
|
||||
contentAccessibility: true,
|
||||
documentAssembly: false
|
||||
}
|
||||
});
|
||||
const stream = doc.pipe(blobStream());
|
||||
for (let i = 0; i < pageImages.length; i++) {
|
||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
|
||||
}
|
||||
doc.end();
|
||||
|
||||
stream.on('finish', function () {
|
||||
const blob = stream.toBlob('application/pdf');
|
||||
downloadFile(blob, `encrypted-${file.name}`);
|
||||
hideLoader();
|
||||
showAlert('Success', 'Encryption complete! Your download has started.');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error during PDF encryption:", error);
|
||||
hideLoader();
|
||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||
}
|
||||
}
|
||||
61
src/js/logic/extract-pages.ts
Normal file
61
src/js/logic/extract-pages.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function extractPages() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageInput = document.getElementById('pages-to-extract').value;
|
||||
if (!pageInput.trim()) {
|
||||
showAlert('Invalid Input', 'Please enter page numbers to extract.');
|
||||
return;
|
||||
}
|
||||
showLoader('Extracting pages...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
const indicesToExtract = new Set();
|
||||
const ranges = pageInput.split(',');
|
||||
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToExtract.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (indicesToExtract.size === 0) {
|
||||
showAlert('Invalid Input', 'No valid pages selected for extraction.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
const zip = new JSZip();
|
||||
// @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 sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
|
||||
|
||||
for (const index of sortedIndices) {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
|
||||
newPdf.addPage(copiedPage);
|
||||
const newPdfBytes = await newPdf.save();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'extracted-pages.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not extract pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
88
src/js/logic/fix-dimensions.ts
Normal file
88
src/js/logic/fix-dimensions.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
|
||||
export function setupFixDimensionsUI() {
|
||||
const targetSizeSelect = document.getElementById('target-size');
|
||||
const customSizeWrapper = document.getElementById('custom-size-wrapper');
|
||||
if (targetSizeSelect && customSizeWrapper) {
|
||||
targetSizeSelect.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
customSizeWrapper.classList.toggle('hidden', targetSizeSelect.value !== 'Custom');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function fixDimensions() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const targetSizeKey = document.getElementById('target-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const orientation = document.getElementById('orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const scalingMode = document.querySelector('input[name="scaling-mode"]:checked').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const backgroundColor = hexToRgb(document.getElementById('background-color').value);
|
||||
|
||||
showLoader('Standardizing pages...');
|
||||
try {
|
||||
let targetWidth, targetHeight;
|
||||
|
||||
if (targetSizeKey === 'Custom') {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const width = parseFloat(document.getElementById('custom-width').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const height = parseFloat(document.getElementById('custom-height').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const units = document.getElementById('custom-units').value;
|
||||
if (units === 'in') {
|
||||
targetWidth = width * 72;
|
||||
targetHeight = height * 72;
|
||||
} else { // mm
|
||||
targetWidth = width * (72 / 25.4);
|
||||
targetHeight = height * (72 / 25.4);
|
||||
}
|
||||
} else {
|
||||
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
|
||||
}
|
||||
|
||||
if (orientation === 'landscape' && targetWidth < targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
|
||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||
}
|
||||
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const sourcePage of sourceDoc.getPages()) {
|
||||
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||
newPage.drawRectangle({ x: 0, y: 0, width: targetWidth, height: targetHeight, color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b) });
|
||||
|
||||
const scaleX = targetWidth / sourceWidth;
|
||||
const scaleY = targetHeight / sourceHeight;
|
||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
||||
|
||||
const scaledWidth = sourceWidth * scale;
|
||||
const scaledHeight = sourceHeight * scale;
|
||||
|
||||
const x = (targetWidth - scaledWidth) / 2;
|
||||
const y = (targetHeight - scaledHeight) / 2;
|
||||
|
||||
newPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'standardized.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while standardizing pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
27
src/js/logic/flatten.ts
Normal file
27
src/js/logic/flatten.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function flatten() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Flattening PDF...');
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
form.flatten();
|
||||
|
||||
const flattenedBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([flattenedBytes], { type: 'application/pdf' }), 'flattened.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e.message.includes('getForm')) {
|
||||
showAlert('No Form Found', 'This PDF does not contain any form fields to flatten.');
|
||||
} else {
|
||||
showAlert('Error', 'Could not flatten the PDF.');
|
||||
}
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
321
src/js/logic/form-filler.ts
Normal file
321
src/js/logic/form-filler.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import {
|
||||
PDFDocument as PDFLibDocument,
|
||||
PDFTextField,
|
||||
PDFCheckBox,
|
||||
PDFRadioGroup,
|
||||
PDFDropdown,
|
||||
PDFButton,
|
||||
PDFSignature,
|
||||
PDFOptionList
|
||||
} 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 pdfJsDoc: any = null;
|
||||
let currentPageNum = 1;
|
||||
let pdfRendering = false;
|
||||
let renderTimeout: any = null;
|
||||
const formState = {
|
||||
scale: 2,
|
||||
fields: [],
|
||||
};
|
||||
|
||||
let fieldValues: Record<string, any> = {};
|
||||
|
||||
async function renderPage() {
|
||||
if (pdfRendering || !pdfJsDoc) return;
|
||||
|
||||
pdfRendering = true;
|
||||
showLoader(`Rendering page ${currentPageNum}...`);
|
||||
|
||||
const page = await pdfJsDoc.getPage(currentPageNum);
|
||||
const viewport = page.getViewport({ scale: 1.0 });
|
||||
|
||||
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
if (!context) {
|
||||
console.error('Could not get canvas context');
|
||||
pdfRendering = false;
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
canvas.style.transformOrigin = 'top left';
|
||||
canvas.style.transform = `scale(${formState.scale})`;
|
||||
|
||||
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), { ignoreEncryption: true });
|
||||
const form = tempPdfDoc.getForm();
|
||||
Object.keys(fieldValues).forEach(fieldName => {
|
||||
try {
|
||||
const field = form.getField(fieldName);
|
||||
if (!field) return;
|
||||
|
||||
if (field instanceof PDFTextField) {
|
||||
field.setText(fieldValues[fieldName]);
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
if (fieldValues[fieldName] === 'on') {
|
||||
field.check();
|
||||
} else {
|
||||
field.uncheck();
|
||||
}
|
||||
} else if (field instanceof PDFRadioGroup) {
|
||||
field.select(fieldValues[fieldName]);
|
||||
} else if (field instanceof PDFDropdown) {
|
||||
field.select(fieldValues[fieldName]);
|
||||
} else if (field instanceof PDFOptionList) {
|
||||
// Handle multi-select list box
|
||||
if (Array.isArray(fieldValues[fieldName])) {
|
||||
fieldValues[fieldName].forEach((option: any) => field.select(option));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error applying value to field "${fieldName}":`, e);
|
||||
}
|
||||
});
|
||||
|
||||
const tempPdfBytes = await tempPdfDoc.save();
|
||||
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes }).promise;
|
||||
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
||||
|
||||
// Use the newer PDF.js render API
|
||||
await tempPage.render({
|
||||
canvasContext: context,
|
||||
viewport: viewport
|
||||
} as any).promise;
|
||||
|
||||
const currentPageDisplay = document.getElementById('current-page-display');
|
||||
const totalPagesDisplay = document.getElementById('total-pages-display');
|
||||
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
||||
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
|
||||
|
||||
if (currentPageDisplay) currentPageDisplay.textContent = String(currentPageNum);
|
||||
if (totalPagesDisplay) totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
|
||||
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
|
||||
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
|
||||
|
||||
pdfRendering = false;
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the next or previous page.
|
||||
* @param {number} offset 1 for next, -1 for previous.
|
||||
*/
|
||||
async function changePage(offset: number) {
|
||||
const newPageNum = currentPageNum + offset;
|
||||
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
|
||||
currentPageNum = newPageNum;
|
||||
await renderPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the zoom level of the PDF viewer.
|
||||
* @param {number} factor The zoom factor.
|
||||
*/
|
||||
async function setZoom(factor: number) {
|
||||
formState.scale = factor;
|
||||
await renderPage();
|
||||
}
|
||||
|
||||
function handleFormChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement | HTMLSelectElement;
|
||||
const name = input.name;
|
||||
let value: any;
|
||||
|
||||
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
|
||||
value = input.checked ? 'on' : 'off';
|
||||
} else if (input instanceof HTMLSelectElement && input.multiple) {
|
||||
// Handle multi-select list box
|
||||
value = Array.from(input.options)
|
||||
.filter(option => option.selected)
|
||||
.map(option => option.value);
|
||||
} else {
|
||||
value = input.value;
|
||||
}
|
||||
|
||||
fieldValues[name] = value;
|
||||
|
||||
clearTimeout(renderTimeout);
|
||||
renderTimeout = setTimeout(() => {
|
||||
renderPage();
|
||||
}, 350);
|
||||
}
|
||||
|
||||
function createFormFieldHtml(field: any) {
|
||||
const name = field.getName();
|
||||
const isRequired = field.isRequired();
|
||||
|
||||
const labelText = name.replace(/[_-]/g, ' ');
|
||||
const labelHtml = `<label for="field-${name}" class="block text-sm font-medium text-gray-300 capitalize mb-1">${labelText} ${isRequired ? '<span class="text-red-500">*</span>' : ''}</label>`;
|
||||
|
||||
let inputHtml = '';
|
||||
|
||||
if (field instanceof PDFTextField) {
|
||||
fieldValues[name] = field.getText() || '';
|
||||
inputHtml = `<input type="text" id="field-${name}" name="${name}" value="${fieldValues[name]}" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">`;
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
||||
inputHtml = `<input type="checkbox" id="field-${name}" name="${name}" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500" ${field.isChecked() ? 'checked' : ''}>`;
|
||||
} else if (field instanceof PDFRadioGroup) {
|
||||
fieldValues[name] = field.getSelected();
|
||||
const options = field.getOptions();
|
||||
inputHtml = options.map((opt: any) => `
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="radio" name="${name}" value="${opt}" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500" ${opt === field.getSelected() ? 'checked' : ''}>
|
||||
<span class="text-gray-300 text-sm">${opt}</span>
|
||||
</label>
|
||||
`).join('');
|
||||
} else if (field instanceof PDFDropdown) {
|
||||
fieldValues[name] = field.getSelected();
|
||||
const dropdownOptions = field.getOptions();
|
||||
inputHtml = `<select id="field-${name}" name="${name}" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
|
||||
${dropdownOptions.map((opt: any) => `<option value="${opt}" ${opt === field.getSelected() ? 'selected' : ''}>${opt}</option>`).join('')}
|
||||
</select>`;
|
||||
} else if (field instanceof PDFOptionList) {
|
||||
const selectedValues = field.getSelected();
|
||||
fieldValues[name] = selectedValues;
|
||||
const listOptions = field.getOptions();
|
||||
inputHtml = `<select id="field-${name}" name="${name}" multiple size="${Math.min(10, listOptions.length)}" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 h-auto">
|
||||
${listOptions.map((opt: any) => `<option value="${opt}" ${selectedValues.includes(opt) ? 'selected' : ''}>${opt}</option>`).join('')}
|
||||
</select>`;
|
||||
} else if (field instanceof PDFSignature) {
|
||||
inputHtml = `<div class="p-4 bg-gray-800 rounded-lg border border-gray-700"><p class="text-sm text-gray-400">Signature field: Not supported for direct editing.</p></div>`;
|
||||
} else if (field instanceof PDFButton) {
|
||||
inputHtml = `<button type="button" id="field-${name}" class="btn bg-gray-700 hover:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg">Button: ${labelText}</button>`;
|
||||
} else {
|
||||
return `<div class="p-4 bg-gray-800 rounded-lg border border-gray-700"><p class="text-sm text-gray-500">Unsupported field type: ${field.constructor.name}</p></div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700">
|
||||
${labelHtml}
|
||||
${inputHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export async function setupFormFiller() {
|
||||
if (!state.pdfDoc) return;
|
||||
|
||||
showLoader('Analyzing form fields...');
|
||||
const formContainer = document.getElementById('form-fields-container');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
if (!formContainer || !processBtn) {
|
||||
console.error('Required DOM elements not found');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
const fields = form.getFields();
|
||||
formState.fields = fields;
|
||||
|
||||
if (fields.length === 0) {
|
||||
formContainer.innerHTML = '<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
||||
processBtn.classList.add('hidden');
|
||||
} else {
|
||||
let formHtml = '';
|
||||
|
||||
fields.forEach((field: any) => {
|
||||
try {
|
||||
formHtml += createFormFieldHtml(field);
|
||||
} catch (e: any) {
|
||||
console.error(`Error processing field "${field.getName()}":`, e);
|
||||
formHtml += `<div class="p-4 bg-gray-800 rounded-lg border border-gray-700"><p class="text-sm text-gray-500">Unsupported field: ${field.getName()}</p><p class="text-xs text-gray-500">${e.message}</p></div>`;
|
||||
}
|
||||
});
|
||||
|
||||
formContainer.innerHTML = formHtml;
|
||||
processBtn.classList.remove('hidden');
|
||||
|
||||
formContainer.addEventListener('change', handleFormChange);
|
||||
formContainer.addEventListener('input', handleFormChange);
|
||||
}
|
||||
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
currentPageNum = 1;
|
||||
await renderPage();
|
||||
|
||||
const zoomInBtn = document.getElementById('zoom-in-btn');
|
||||
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
||||
const prevPageBtn = document.getElementById('prev-page');
|
||||
const nextPageBtn = document.getElementById('next-page');
|
||||
|
||||
if (zoomInBtn) zoomInBtn.onclick = () => setZoom(formState.scale + 0.25);
|
||||
if (zoomOutBtn) zoomOutBtn.onclick = () => setZoom(Math.max(0.25, formState.scale - 0.25));
|
||||
if (prevPageBtn) prevPageBtn.onclick = () => changePage(-1);
|
||||
if (nextPageBtn) nextPageBtn.onclick = () => changePage(1);
|
||||
|
||||
hideLoader();
|
||||
|
||||
const formFillerOptions = document.getElementById('form-filler-options');
|
||||
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
||||
|
||||
} catch (e) {
|
||||
console.error("Critical error setting up form filler:", e);
|
||||
showAlert('Error', 'Failed to read PDF form data. The file may be corrupt or not a valid form.');
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export async function processAndDownloadForm() {
|
||||
showLoader('Applying form data...');
|
||||
try {
|
||||
const form = state.pdfDoc.getForm();
|
||||
|
||||
Object.keys(fieldValues).forEach(fieldName => {
|
||||
try {
|
||||
const field = form.getField(fieldName);
|
||||
const value = fieldValues[fieldName];
|
||||
|
||||
if (field instanceof PDFTextField) {
|
||||
field.setText(value);
|
||||
} else if (field instanceof PDFCheckBox) {
|
||||
if (value === 'on') {
|
||||
field.check();
|
||||
} else {
|
||||
field.uncheck();
|
||||
}
|
||||
} else if (field instanceof PDFRadioGroup) {
|
||||
field.select(value);
|
||||
} else if (field instanceof PDFDropdown) {
|
||||
field.select(value);
|
||||
} else if (field instanceof PDFOptionList) {
|
||||
// Handle multi-select list box
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(option => field.select(option));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error processing field "${fieldName}" during download:`, e);
|
||||
}
|
||||
});
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'filled-form.pdf');
|
||||
|
||||
showAlert('Success', 'Form has been filled and downloaded.');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to save the filled form.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
36
src/js/logic/heic-to-pdf.ts
Normal file
36
src/js/logic/heic-to-pdf.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import heic2any from 'heic2any';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function heicToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one HEIC file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting HEIC to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const conversionResult = await heic2any({
|
||||
blob: file,
|
||||
toType: "image/png",
|
||||
});
|
||||
const pngBlob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult;
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'from_heic.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
133
src/js/logic/image-to-pdf.ts
Normal file
133
src/js/logic/image-to-pdf.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
/**
|
||||
* Converts any image into a standard, web-friendly JPEG. Loses transparency.
|
||||
* @param {Uint8Array} imageBytes The raw bytes of the image file.
|
||||
* @returns {Promise<Uint8Array>} A promise that resolves with sanitized JPEG bytes.
|
||||
*/
|
||||
function sanitizeImageAsJpeg(imageBytes: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([imageBytes]);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toBlob(
|
||||
async (jpegBlob) => {
|
||||
if (!jpegBlob) return reject(new Error('Canvas to JPEG conversion failed.'));
|
||||
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
|
||||
}, 'image/jpeg', 0.9
|
||||
);
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(new Error('File could not be loaded as an image.'));
|
||||
};
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts any image into a standard PNG. Preserves transparency.
|
||||
* @param {Uint8Array} imageBytes The raw bytes of the image file.
|
||||
* @returns {Promise<Uint8Array>} A promise that resolves with sanitized PNG bytes.
|
||||
*/
|
||||
function sanitizeImageAsPng(imageBytes: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([imageBytes]);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toBlob(
|
||||
async (pngBlob) => {
|
||||
if (!pngBlob) return reject(new Error('Canvas to PNG conversion failed.'));
|
||||
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
|
||||
}, 'image/png'
|
||||
);
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(new Error('File could not be loaded as an image.'));
|
||||
};
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function imageToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one image file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting images to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
const imageList = document.getElementById('image-list');
|
||||
const sortedFiles = Array.from(imageList.children)
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
.map(li => state.files.find(f => f.name === li.dataset.fileName))
|
||||
.filter(Boolean);
|
||||
|
||||
for (const file of sortedFiles) {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
let image;
|
||||
|
||||
if (file.type === 'image/jpeg') {
|
||||
try {
|
||||
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
|
||||
} catch (e) {
|
||||
console.warn(`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`);
|
||||
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
|
||||
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
} else if (file.type === 'image/png') {
|
||||
try {
|
||||
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
|
||||
} catch (e) {
|
||||
console.warn(`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`);
|
||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
} else {
|
||||
// For WebP and other types, convert to PNG to preserve transparency
|
||||
console.warn(`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`);
|
||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||
}
|
||||
|
||||
const page = pdfDoc.addPage([image.width, image.height]);
|
||||
page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
||||
}
|
||||
|
||||
if (pdfDoc.getPageCount() === 0) {
|
||||
throw new Error("No valid images could be processed. Please check your files.");
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'from-images.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
110
src/js/logic/index.ts
Normal file
110
src/js/logic/index.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { merge, setupMergeTool } from './merge.js';
|
||||
import { setupSplitTool, split } from './split.js';
|
||||
import { encrypt } from './encrypt.js';
|
||||
import { decrypt } from './decrypt.js';
|
||||
import { organize } from './organize.js';
|
||||
import { rotate } from './rotate.js';
|
||||
import { addPageNumbers } from './add-page-numbers.js';
|
||||
import { pdfToJpg } from './pdf-to-jpg.js';
|
||||
import { jpgToPdf } from './jpg-to-pdf.js';
|
||||
import { scanToPdf } from './scan-to-pdf.js';
|
||||
import { compress } from './compress.js';
|
||||
import { pdfToGreyscale } from './pdf-to-greyscale.js';
|
||||
import { pdfToZip } from './pdf-to-zip.js';
|
||||
import { editMetadata } from './edit-metadata.js';
|
||||
import { removeMetadata } from './remove-metadata.js';
|
||||
import { flatten } from './flatten.js';
|
||||
import { pdfToPng } from './pdf-to-png.js';
|
||||
import { pngToPdf } from './png-to-pdf.js';
|
||||
import { pdfToWebp } from './pdf-to-webp.js';
|
||||
import { webpToPdf } from './webp-to-pdf.js';
|
||||
import { deletePages } from './delete-pages.js';
|
||||
import { addBlankPage } from './add-blank-page.js';
|
||||
import { extractPages } from './extract-pages.js';
|
||||
import { addWatermark, setupWatermarkUI } from './add-watermark.js';
|
||||
import { addHeaderFooter } from './add-header-footer.js';
|
||||
import { imageToPdf } from './image-to-pdf.js';
|
||||
import { changePermissions } from './change-permissions.js';
|
||||
import { pdfToMarkdown } from './pdf-to-markdown.js';
|
||||
import { txtToPdf } from './txt-to-pdf.js';
|
||||
import { invertColors } from './invert-colors.js';
|
||||
// import { viewMetadata } from './view-metadata.js';
|
||||
import { reversePages } from './reverse-pages.js';
|
||||
import { mdToPdf } from './md-to-pdf.js';
|
||||
import { svgToPdf } from './svg-to-pdf.js';
|
||||
import { bmpToPdf } from './bmp-to-pdf.js';
|
||||
import { heicToPdf } from './heic-to-pdf.js';
|
||||
import { tiffToPdf } from './tiff-to-pdf.js';
|
||||
import { pdfToBmp } from './pdf-to-bmp.js';
|
||||
import { pdfToTiff } from './pdf-to-tiff.js';
|
||||
import { splitInHalf } from './split-in-half.js';
|
||||
import { analyzeAndDisplayDimensions } from './page-dimensions.js';
|
||||
import { nUpTool, setupNUpUI } from './n-up.js';
|
||||
import { processAndSave } from './duplicate-organize.js';
|
||||
import { combineToSinglePage } from './combine-single-page.js';
|
||||
import { fixDimensions, setupFixDimensionsUI } from './fix-dimensions.js';
|
||||
import { changeBackgroundColor } from './change-background-color.js';
|
||||
import { changeTextColor, setupTextColorTool } from './change-text-color.js';
|
||||
import { setupCompareTool } from './compare-pdfs.js';
|
||||
import { setupOcrTool } from './ocr-pdf.js';
|
||||
import { wordToPdf } from './word-to-pdf.js';
|
||||
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
|
||||
import { removeAnnotations, setupRemoveAnnotationsTool } from './remove-annotations.js';
|
||||
import { setupCropperTool } from './cropper.js';
|
||||
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
|
||||
|
||||
export const toolLogic = {
|
||||
merge: { process: merge, setup: setupMergeTool },
|
||||
split: { process: split, setup: setupSplitTool },
|
||||
encrypt,
|
||||
decrypt,
|
||||
organize,
|
||||
rotate,
|
||||
'add-page-numbers': addPageNumbers,
|
||||
'pdf-to-jpg': pdfToJpg,
|
||||
'jpg-to-pdf': jpgToPdf,
|
||||
'scan-to-pdf': scanToPdf,
|
||||
compress,
|
||||
'pdf-to-greyscale': pdfToGreyscale,
|
||||
'pdf-to-zip': pdfToZip,
|
||||
'edit-metadata': editMetadata,
|
||||
'remove-metadata': removeMetadata,
|
||||
flatten,
|
||||
'pdf-to-png': pdfToPng,
|
||||
'png-to-pdf': pngToPdf,
|
||||
'pdf-to-webp': pdfToWebp,
|
||||
'webp-to-pdf': webpToPdf,
|
||||
'delete-pages': deletePages,
|
||||
'add-blank-page': addBlankPage,
|
||||
'extract-pages': extractPages,
|
||||
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
||||
'add-header-footer': addHeaderFooter,
|
||||
'image-to-pdf': imageToPdf,
|
||||
'change-permissions': changePermissions,
|
||||
'pdf-to-markdown': pdfToMarkdown,
|
||||
'txt-to-pdf': txtToPdf,
|
||||
'invert-colors': invertColors,
|
||||
'reverse-pages': reversePages,
|
||||
'md-to-pdf': mdToPdf,
|
||||
'svg-to-pdf': svgToPdf,
|
||||
'bmp-to-pdf': bmpToPdf,
|
||||
'heic-to-pdf': heicToPdf,
|
||||
'tiff-to-pdf': tiffToPdf,
|
||||
'pdf-to-bmp': pdfToBmp,
|
||||
'pdf-to-tiff': pdfToTiff,
|
||||
'split-in-half': splitInHalf,
|
||||
'page-dimensions': analyzeAndDisplayDimensions,
|
||||
'n-up': { process: nUpTool, setup: setupNUpUI },
|
||||
'duplicate-organize': { process: processAndSave },
|
||||
'combine-single-page': combineToSinglePage,
|
||||
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
|
||||
'change-background-color': changeBackgroundColor,
|
||||
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
|
||||
'compare-pdfs': { setup: setupCompareTool },
|
||||
'ocr-pdf': { setup: setupOcrTool },
|
||||
'word-to-pdf': wordToPdf,
|
||||
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
|
||||
'remove-annotations': { process: removeAnnotations, setup: setupRemoveAnnotationsTool },
|
||||
'cropper': { setup: setupCropperTool },
|
||||
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller},
|
||||
};
|
||||
53
src/js/logic/invert-colors.ts
Normal file
53
src/js/logic/invert-colors.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// FILE: js/logic/invert-colors.js
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function invertColors() {
|
||||
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
|
||||
showLoader('Inverting PDF colors...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
data[j] = 255 - data[j]; // red
|
||||
data[j + 1] = 255 - data[j + 1]; // green
|
||||
data[j + 2] = 255 - data[j + 2]; // blue
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png'));
|
||||
|
||||
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'inverted.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not invert PDF colors.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
93
src/js/logic/jpg-to-pdf.ts
Normal file
93
src/js/logic/jpg-to-pdf.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
/**
|
||||
* Takes any image byte array and uses the browser's canvas to convert it
|
||||
* into a standard, web-friendly (baseline, sRGB) JPEG byte array.
|
||||
* @param {Uint8Array} imageBytes The raw bytes of the image file.
|
||||
* @returns {Promise<Uint8Array>} A promise that resolves with the sanitized JPEG bytes.
|
||||
*/
|
||||
function sanitizeImageAsJpeg(imageBytes: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const blob = new Blob([imageBytes]);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth;
|
||||
canvas.height = img.naturalHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
canvas.toBlob(
|
||||
async (jpegBlob) => {
|
||||
if (!jpegBlob) {
|
||||
return reject(new Error('Canvas toBlob conversion failed.'));
|
||||
}
|
||||
const arrayBuffer = await jpegBlob.arrayBuffer();
|
||||
resolve(new Uint8Array(arrayBuffer));
|
||||
},
|
||||
'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
reject(new Error('The provided file could not be loaded as an image. It may be corrupted.'));
|
||||
};
|
||||
|
||||
img.src = imageUrl;
|
||||
});
|
||||
}
|
||||
|
||||
export async function jpgToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one JPG file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Creating PDF from JPGs...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
|
||||
for (const file of state.files) {
|
||||
const originalBytes = await readFileAsArrayBuffer(file);
|
||||
let jpgImage;
|
||||
|
||||
try {
|
||||
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
||||
} catch (e) {
|
||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
showAlert(`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`);
|
||||
try {
|
||||
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
||||
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||
} catch (fallbackError) {
|
||||
console.error(`Failed to process ${file.name} after sanitization:`, fallbackError);
|
||||
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
|
||||
}
|
||||
}
|
||||
|
||||
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
|
||||
page.drawImage(jpgImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: jpgImage.width,
|
||||
height: jpgImage.height,
|
||||
});
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'from_jpgs.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Conversion Error', e.message);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
85
src/js/logic/md-to-pdf.ts
Normal file
85
src/js/logic/md-to-pdf.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
// note: this is a work in progress
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
|
||||
export async function mdToPdf() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||
if (typeof window.jspdf === 'undefined' || typeof window.html2canvas === 'undefined') {
|
||||
showAlert('Libraries Not Ready', 'PDF generation libraries are loading. Please try again.');
|
||||
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 markdownContent = document.getElementById('md-input').value.trim();
|
||||
if (!markdownContent) {
|
||||
showAlert('Input Required', 'Please enter some Markdown text.');
|
||||
return;
|
||||
}
|
||||
showLoader('Generating High-Quality PDF...');
|
||||
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
|
||||
const htmlContent = marked.parse(markdownContent);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageFormat = document.getElementById('page-format').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const orientation = document.getElementById('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
|
||||
const marginSize = document.getElementById('margin-size').value;
|
||||
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.style.cssText = 'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = `
|
||||
body { font-family: Helvetica, Arial, sans-serif; line-height: 1.6; font-size: 12px; }
|
||||
h1, h2, h3 { margin: 20px 0 10px 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
|
||||
h1 { font-size: 2em; } h2 { font-size: 1.5em; }
|
||||
p, blockquote, ul, ol, pre, table { margin: 0 0 16px 0; }
|
||||
blockquote { padding: 0 1em; color: #6a737d; border-left: .25em solid #dfe2e5; }
|
||||
pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 6px; }
|
||||
code { font-family: 'Courier New', monospace; background-color: rgba(27,31,35,.05); border-radius: 3px; padding: .2em .4em; }
|
||||
table { width: 100%; border-collapse: collapse; } th, td { padding: 6px 13px; border: 1px solid #dfe2e5; }
|
||||
img { max-width: 100%; }
|
||||
`;
|
||||
tempContainer.appendChild(styleSheet);
|
||||
tempContainer.innerHTML += htmlContent;
|
||||
document.body.appendChild(tempContainer);
|
||||
|
||||
const canvas = await html2canvas(tempContainer, { scale: 2, useCORS: true });
|
||||
document.body.removeChild(tempContainer);
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||
const { jsPDF } = window.jspdf;
|
||||
const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
|
||||
const pageFormats = { 'a4': [210, 297], 'letter': [216, 279] };
|
||||
const format = pageFormats[pageFormat];
|
||||
const [pageWidth, pageHeight] = orientation === 'landscape' ? [format[1], format[0]] : format;
|
||||
const margins = { 'narrow': 10, 'normal': 20, 'wide': 30 };
|
||||
const margin = margins[marginSize];
|
||||
const contentWidth = pageWidth - (margin * 2);
|
||||
const contentHeight = pageHeight - (margin * 2);
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const imgHeight = (canvas.height * contentWidth) / canvas.width;
|
||||
|
||||
let heightLeft = imgHeight;
|
||||
let position = margin;
|
||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||
heightLeft -= contentHeight;
|
||||
|
||||
while (heightLeft > 0) {
|
||||
position = position - pageHeight;
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||
heightLeft -= contentHeight;
|
||||
}
|
||||
|
||||
const pdfBlob = pdf.output('blob');
|
||||
downloadFile(pdfBlob, 'markdown-document.pdf');
|
||||
} catch (error) {
|
||||
console.error('MD to PDF conversion error:', error);
|
||||
showAlert('Conversion Error', 'Failed to generate PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
396
src/js/logic/merge.ts
Normal file
396
src/js/logic/merge.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts';
|
||||
import { state } from '../state.ts';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const mergeState = {
|
||||
pdfDocs: {},
|
||||
activeMode: 'file',
|
||||
sortableInstances: {},
|
||||
isRendering: false,
|
||||
cachedThumbnails: null,
|
||||
lastFileHash: null
|
||||
};
|
||||
|
||||
function parsePageRanges(rangeString: any, totalPages: any) {
|
||||
const indices = new Set();
|
||||
if (!rangeString.trim()) return [];
|
||||
|
||||
const ranges = rangeString.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) {
|
||||
indices.add(i - 1);
|
||||
}
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indices.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
return Array.from(indices).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function initializeFileListSortable() {
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) return;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
if (mergeState.sortableInstances.fileList) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
mergeState.sortableInstances.fileList.destroy();
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
onStart: function (evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function (evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initializePageThumbnailsSortable() {
|
||||
const container = document.getElementById('page-merge-preview');
|
||||
if (!container) return;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
if (mergeState.sortableInstances.pageThumbnails) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
mergeState.sortableInstances.pageThumbnails.destroy();
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
chosenClass: 'sortable-chosen',
|
||||
dragClass: 'sortable-drag',
|
||||
onStart: function (evt: any) {
|
||||
evt.item.style.opacity = '0.5';
|
||||
},
|
||||
onEnd: function (evt: any) {
|
||||
evt.item.style.opacity = '1';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function generateFileHash() {
|
||||
return (state.files as File[]).map(f => `${f.name}-${f.size}-${f.lastModified}`).join('|');
|
||||
}
|
||||
|
||||
async function renderPageMergeThumbnails() {
|
||||
const container = document.getElementById('page-merge-preview');
|
||||
if (!container) return;
|
||||
|
||||
const currentFileHash = generateFileHash();
|
||||
const filesChanged = currentFileHash !== mergeState.lastFileHash;
|
||||
|
||||
if (!filesChanged && mergeState.cachedThumbnails !== null) {
|
||||
// Simple check to see if it's already rendered to avoid flicker.
|
||||
if (container.firstChild) {
|
||||
initializePageThumbnailsSortable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mergeState.isRendering) {
|
||||
return;
|
||||
}
|
||||
|
||||
mergeState.isRendering = true;
|
||||
container.textContent = '';
|
||||
|
||||
let currentPageNumber = 0;
|
||||
let totalPages = state.files.reduce((sum, file) => {
|
||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
||||
}, 0);
|
||||
|
||||
try {
|
||||
const thumbnailsHTML = [];
|
||||
|
||||
for (const file of state.files) {
|
||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||
if (!pdfDoc) continue;
|
||||
|
||||
const pdfData = await pdfDoc.save();
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
currentPageNumber++;
|
||||
showLoader(`Rendering page previews: ${currentPageNumber}/${totalPages}`);
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.3 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d')!;
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
canvas: canvas,
|
||||
viewport
|
||||
}).promise;
|
||||
|
||||
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 = file.name;
|
||||
wrapper.dataset.pageIndex = (i - 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 = 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.lastFileHash = currentFileHash;
|
||||
|
||||
initializePageThumbnailsSortable();
|
||||
} catch (error) {
|
||||
console.error('Error rendering page thumbnails:', error);
|
||||
showAlert('Error', 'Failed to render page thumbnails');
|
||||
} finally {
|
||||
hideLoader();
|
||||
mergeState.isRendering = false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function merge() {
|
||||
showLoader('Merging PDFs...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
|
||||
if (mergeState.activeMode === 'file') {
|
||||
const fileList = document.getElementById('file-list');
|
||||
const sortedFiles = Array.from(fileList.children).map(li => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
return state.files.find(f => f.name === li.dataset.fileName);
|
||||
}).filter(Boolean);
|
||||
|
||||
for (const file of sortedFiles) {
|
||||
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const rangeInput = document.getElementById(`range-${safeFileName}`);
|
||||
if (!rangeInput) continue;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const rangeInputValue = rangeInput.value;
|
||||
const sourcePdf = mergeState.pdfDocs[file.name];
|
||||
if (!sourcePdf) continue;
|
||||
|
||||
const totalPages = sourcePdf.getPageCount();
|
||||
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
|
||||
|
||||
const indicesToCopy = pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
||||
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||
}
|
||||
|
||||
} else {
|
||||
const pageContainer = document.getElementById('page-merge-preview');
|
||||
const pageElements = Array.from(pageContainer.children);
|
||||
|
||||
for (const el of pageElements) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const fileName = el.dataset.fileName;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndex = parseInt(el.dataset.pageIndex, 10);
|
||||
|
||||
const sourcePdf = mergeState.pdfDocs[fileName];
|
||||
if (sourcePdf && !isNaN(pageIndex)) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [pageIndex]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mergedPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([mergedPdfBytes], { type: 'application/pdf' }), 'merged.pdf');
|
||||
showAlert('Success', 'PDFs merged successfully!');
|
||||
|
||||
} catch (e) {
|
||||
console.error('Merge error:', e);
|
||||
showAlert('Error', 'Failed to merge PDFs. Please check that all files are valid and not password-protected.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupMergeTool() {
|
||||
document.getElementById('merge-options').classList.remove('hidden');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('process-btn').disabled = false;
|
||||
|
||||
const wasInPageMode = mergeState.activeMode === 'page';
|
||||
|
||||
showLoader('Loading PDF documents...');
|
||||
try {
|
||||
for (const file of state.files) {
|
||||
if (!mergeState.pdfDocs[file.name]) {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading PDFs:', error);
|
||||
showAlert('Error', 'Failed to load one or more PDF files');
|
||||
return;
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
const fileModeBtn = document.getElementById('file-mode-btn');
|
||||
const pageModeBtn = document.getElementById('page-mode-btn');
|
||||
const filePanel = document.getElementById('file-mode-panel');
|
||||
const pagePanel = document.getElementById('page-mode-panel');
|
||||
const fileList = document.getElementById('file-list');
|
||||
|
||||
fileList.textContent = ''; // Clear list safely
|
||||
(state.files as File[]).forEach(f => {
|
||||
const doc = mergeState.pdfDocs[f.name];
|
||||
const pageCount = doc ? doc.getPageCount() : 'N/A';
|
||||
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
||||
li.dataset.fileName = f.name;
|
||||
|
||||
const mainDiv = document.createElement('div');
|
||||
mainDiv.className = 'flex items-center justify-between';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate font-medium text-white flex-1 mr-2';
|
||||
nameSpan.title = f.name;
|
||||
nameSpan.textContent = f.name;
|
||||
|
||||
const dragHandle = document.createElement('div');
|
||||
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
|
||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
|
||||
|
||||
mainDiv.append(nameSpan, dragHandle);
|
||||
|
||||
const rangeDiv = document.createElement('div');
|
||||
rangeDiv.className = 'mt-2';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.htmlFor = `range-${safeFileName}`;
|
||||
label.className = 'text-xs text-gray-400';
|
||||
label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.id = `range-${safeFileName}`;
|
||||
input.className = 'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
||||
input.placeholder = 'Leave blank for all pages';
|
||||
|
||||
rangeDiv.append(label, input);
|
||||
li.append(mainDiv, rangeDiv);
|
||||
fileList.appendChild(li);
|
||||
});
|
||||
|
||||
initializeFileListSortable();
|
||||
|
||||
const newFileModeBtn = fileModeBtn.cloneNode(true);
|
||||
const newPageModeBtn = pageModeBtn.cloneNode(true);
|
||||
fileModeBtn.replaceWith(newFileModeBtn);
|
||||
pageModeBtn.replaceWith(newPageModeBtn);
|
||||
|
||||
newFileModeBtn.addEventListener('click', () => {
|
||||
if (mergeState.activeMode === 'file') return;
|
||||
|
||||
mergeState.activeMode = 'file';
|
||||
filePanel.classList.remove('hidden');
|
||||
pagePanel.classList.add('hidden');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
});
|
||||
|
||||
newPageModeBtn.addEventListener('click', async () => {
|
||||
if (mergeState.activeMode === 'page') return;
|
||||
|
||||
mergeState.activeMode = 'page';
|
||||
filePanel.classList.add('hidden');
|
||||
pagePanel.classList.remove('hidden');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
|
||||
await renderPageMergeThumbnails();
|
||||
});
|
||||
|
||||
if (wasInPageMode) {
|
||||
mergeState.activeMode = 'page';
|
||||
filePanel.classList.add('hidden');
|
||||
pagePanel.classList.remove('hidden');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
|
||||
await renderPageMergeThumbnails();
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
}
|
||||
}
|
||||
111
src/js/logic/n-up.ts
Normal file
111
src/js/logic/n-up.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||
|
||||
export function setupNUpUI() {
|
||||
const addBorderCheckbox = document.getElementById('add-border');
|
||||
const borderColorWrapper = document.getElementById('border-color-wrapper');
|
||||
if (addBorderCheckbox && borderColorWrapper) {
|
||||
addBorderCheckbox.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function nUpTool() {
|
||||
// 1. Gather all options from the UI
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const n = parseInt(document.getElementById('pages-per-sheet').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageSizeKey = document.getElementById('output-page-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
let orientation = document.getElementById('output-orientation').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const useMargins = document.getElementById('add-margins').checked;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const addBorder = document.getElementById('add-border').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 borderColor = hexToRgb(document.getElementById('border-color').value);
|
||||
|
||||
showLoader('Creating N-Up PDF...');
|
||||
try {
|
||||
const sourceDoc = state.pdfDoc;
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
const sourcePages = sourceDoc.getPages();
|
||||
|
||||
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
|
||||
|
||||
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
|
||||
|
||||
if (orientation === 'auto') {
|
||||
const firstPage = sourcePages[0];
|
||||
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
||||
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
|
||||
orientation = (isSourceLandscape && gridDims[0] > gridDims[1]) ? 'landscape' : 'portrait';
|
||||
}
|
||||
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||
}
|
||||
|
||||
const margin = useMargins ? 36 : 0;
|
||||
const gutter = useMargins ? 10 : 0;
|
||||
|
||||
const usableWidth = pageWidth - (margin * 2);
|
||||
const usableHeight = pageHeight - (margin * 2);
|
||||
|
||||
// Loop through the source pages in chunks of 'n'
|
||||
for (let i = 0; i < sourcePages.length; i += n) {
|
||||
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
|
||||
const chunk = sourcePages.slice(i, i + n);
|
||||
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
||||
|
||||
// Calculate dimensions of each cell in the grid
|
||||
const cellWidth = (usableWidth - (gutter * (gridDims[0] - 1))) / gridDims[0];
|
||||
const cellHeight = (usableHeight - (gutter * (gridDims[1] - 1))) / gridDims[1];
|
||||
|
||||
for (let j = 0; j < chunk.length; j++) {
|
||||
const sourcePage = chunk[j];
|
||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||
|
||||
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
||||
const scale = Math.min(cellWidth / embeddedPage.width, cellHeight / embeddedPage.height);
|
||||
const scaledWidth = embeddedPage.width * scale;
|
||||
const scaledHeight = embeddedPage.height * scale;
|
||||
|
||||
// Calculate position (x, y) for this cell
|
||||
const row = Math.floor(j / gridDims[0]);
|
||||
const col = j % gridDims[0];
|
||||
const cellX = margin + col * (cellWidth + gutter);
|
||||
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
||||
|
||||
// Center the page within its cell
|
||||
const x = cellX + (cellWidth - scaledWidth) / 2;
|
||||
const y = cellY + (cellHeight - scaledHeight) / 2;
|
||||
|
||||
outputPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
||||
|
||||
if (addBorder) {
|
||||
outputPage.drawRectangle({
|
||||
x, y,
|
||||
width: scaledWidth,
|
||||
height: scaledHeight,
|
||||
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
||||
borderWidth: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await newDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), `n-up_${n}.pdf`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
278
src/js/logic/ocr-pdf.ts
Normal file
278
src/js/logic/ocr-pdf.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { tesseractLanguages } from '../config/tesseract-languages.js';
|
||||
import { showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import Tesseract from 'tesseract.js';
|
||||
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||
import { icons, createIcons } from "lucide";
|
||||
|
||||
let searchablePdfBytes: any = null;
|
||||
|
||||
|
||||
function sanitizeTextForWinAnsi(text: string): string {
|
||||
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
|
||||
return text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
|
||||
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
||||
}
|
||||
|
||||
function parseHOCR(hocrText: string) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(hocrText, 'text/html');
|
||||
const words = [];
|
||||
|
||||
// Find all word elements in hOCR
|
||||
const wordElements = doc.querySelectorAll('.ocrx_word');
|
||||
|
||||
wordElements.forEach((wordEl) => {
|
||||
const titleAttr = wordEl.getAttribute('title');
|
||||
const text = wordEl.textContent?.trim() || '';
|
||||
|
||||
if (!titleAttr || !text) return;
|
||||
|
||||
// Parse bbox coordinates from title attribute
|
||||
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
|
||||
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
|
||||
const confMatch = titleAttr.match(/x_wconf (\d+)/);
|
||||
|
||||
if (bboxMatch) {
|
||||
words.push({
|
||||
text: text,
|
||||
bbox: {
|
||||
x0: parseInt(bboxMatch[1]),
|
||||
y0: parseInt(bboxMatch[2]),
|
||||
x1: parseInt(bboxMatch[3]),
|
||||
y1: parseInt(bboxMatch[4])
|
||||
},
|
||||
confidence: confMatch ? parseInt(confMatch[1]) : 0
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
function binarizeCanvas(ctx: any) {
|
||||
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// A simple luminance-based threshold for determining black or white
|
||||
const brightness = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||||
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
||||
data[i] = data[i + 1] = data[i + 2] = color;
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
function updateProgress(status: any, progress: any) {
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressStatus = document.getElementById('progress-status');
|
||||
const progressLog = document.getElementById('progress-log');
|
||||
|
||||
if (!progressBar || !progressStatus || !progressLog) return;
|
||||
|
||||
progressStatus.textContent = status;
|
||||
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
|
||||
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
|
||||
|
||||
const logMessage = `Status: ${status}`;
|
||||
progressLog.textContent += logMessage + '\n';
|
||||
progressLog.scrollTop = progressLog.scrollHeight;
|
||||
}
|
||||
|
||||
|
||||
async function runOCR() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const selectedLangs = Array.from(document.querySelectorAll('.lang-checkbox:checked')).map(cb => cb.value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const scale = parseFloat(document.getElementById('ocr-resolution').value);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const binarize = document.getElementById('ocr-binarize').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 whitelist = document.getElementById('ocr-whitelist').value;
|
||||
|
||||
if (selectedLangs.length === 0) {
|
||||
showAlert('No Languages Selected', 'Please select at least one language for OCR.');
|
||||
return;
|
||||
}
|
||||
const langString = selectedLangs.join('+');
|
||||
|
||||
document.getElementById('ocr-options').classList.add('hidden');
|
||||
document.getElementById('ocr-progress').classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const worker = await Tesseract.createWorker(langString, 1, {
|
||||
logger: (m: any) => updateProgress(m.status, m.progress || 0)
|
||||
});
|
||||
|
||||
// Enable hOCR output
|
||||
await worker.setParameters({
|
||||
tessjs_create_hocr: '1',
|
||||
});
|
||||
|
||||
if (whitelist.trim()) {
|
||||
await worker.setParameters({
|
||||
tessedit_char_whitelist: whitelist.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
let fullText = '';
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
updateProgress(`Processing page ${i} of ${pdf.numPages}`, (i - 1) / pdf.numPages);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport }).promise;
|
||||
|
||||
if (binarize) {
|
||||
binarizeCanvas(context);
|
||||
}
|
||||
|
||||
const result = await worker.recognize(canvas);
|
||||
const data = result.data;
|
||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png'));
|
||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
||||
|
||||
// Parse hOCR to get word-level data
|
||||
if (data.hocr) {
|
||||
const words = parseHOCR(data.hocr);
|
||||
|
||||
words.forEach((word: any) => {
|
||||
const { x0, y0, x1, y1 } = word.bbox;
|
||||
// Sanitize the text to remove characters WinAnsi cannot encode
|
||||
const text = sanitizeTextForWinAnsi(word.text);
|
||||
|
||||
// Skip words that become empty after sanitization
|
||||
if (!text.trim()) return;
|
||||
|
||||
const bboxWidth = x1 - x0;
|
||||
const bboxHeight = y1 - y0;
|
||||
|
||||
let fontSize = bboxHeight * 0.9;
|
||||
let textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
while (textWidth > bboxWidth && fontSize > 1) {
|
||||
fontSize -= 0.5;
|
||||
textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||
}
|
||||
|
||||
try {
|
||||
newPage.drawText(text, {
|
||||
x: x0,
|
||||
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
|
||||
font,
|
||||
size: fontSize,
|
||||
color: rgb(0, 0, 0),
|
||||
opacity: 0,
|
||||
});
|
||||
} catch (error) {
|
||||
// If drawing fails despite sanitization, log and skip this word
|
||||
console.warn(`Could not draw text "${text}":`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fullText += data.text + '\n\n';
|
||||
}
|
||||
|
||||
await worker.terminate();
|
||||
|
||||
searchablePdfBytes = await newPdfDoc.save();
|
||||
document.getElementById('ocr-progress').classList.add('hidden');
|
||||
document.getElementById('ocr-results').classList.remove('hidden');
|
||||
|
||||
createIcons({icons});
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
document.getElementById('ocr-text-output').value = fullText.trim();
|
||||
|
||||
document.getElementById('download-searchable-pdf').addEventListener('click', () => {
|
||||
downloadFile(new Blob([searchablePdfBytes], { type: 'application/pdf' }), 'searchable.pdf');
|
||||
});
|
||||
|
||||
// CHANGE: The copy button logic is updated to be safer.
|
||||
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
|
||||
const button = e.currentTarget as HTMLButtonElement;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme...
|
||||
const textToCopy = document.getElementById('ocr-text-output').value;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
button.textContent = ''; // Clear the button safely
|
||||
const icon = document.createElement('i');
|
||||
icon.setAttribute('data-lucide', 'check');
|
||||
icon.className = 'w-4 h-4 text-green-400';
|
||||
button.appendChild(icon);
|
||||
createIcons({ icons });
|
||||
|
||||
setTimeout(() => {
|
||||
const currentButton = document.getElementById('copy-text-btn');
|
||||
if (currentButton) {
|
||||
currentButton.textContent = ''; // Clear the button safely
|
||||
const resetIcon = document.createElement('i');
|
||||
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
|
||||
resetIcon.className = 'w-4 h-4 text-gray-300';
|
||||
currentButton.appendChild(resetIcon);
|
||||
createIcons({ icons });
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('download-txt-btn').addEventListener('click', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const textToSave = document.getElementById('ocr-text-output').value;
|
||||
const blob = new Blob([textToSave], { type: 'text/plain' });
|
||||
downloadFile(blob, 'ocr-text.txt');
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('OCR Error', 'An error occurred during the OCR process. The worker may have failed to load. Please try again.');
|
||||
document.getElementById('ocr-options').classList.remove('hidden');
|
||||
document.getElementById('ocr-progress').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets up the UI and event listeners for the OCR tool.
|
||||
*/
|
||||
export function setupOcrTool() {
|
||||
const langSearch = document.getElementById('lang-search');
|
||||
const langList = document.getElementById('lang-list');
|
||||
const selectedLangsDisplay = document.getElementById('selected-langs-display');
|
||||
const processBtn = document.getElementById('process-btn');
|
||||
|
||||
langSearch.addEventListener('input', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const searchTerm = langSearch.value.toLowerCase();
|
||||
langList.querySelectorAll('label').forEach(label => {
|
||||
label.style.display = label.textContent.toLowerCase().includes(searchTerm) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Update the display of selected languages
|
||||
langList.addEventListener('change', () => {
|
||||
const selected = Array.from(langList.querySelectorAll('.lang-checkbox:checked'))
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
.map(cb => tesseractLanguages[cb.value]);
|
||||
selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
processBtn.disabled = selected.length === 0;
|
||||
});
|
||||
|
||||
// Attach the main OCR function to the process button
|
||||
processBtn.addEventListener('click', runOCR);
|
||||
}
|
||||
27
src/js/logic/organize.ts
Normal file
27
src/js/logic/organize.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function organize() {
|
||||
showLoader('Saving changes...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageContainer = document.getElementById('page-organizer');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndices = Array.from(pageContainer.children).map(child => parseInt(child.dataset.pageIndex));
|
||||
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'organized.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not save the changes.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
80
src/js/logic/page-dimensions.ts
Normal file
80
src/js/logic/page-dimensions.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { state } from '../state.js';
|
||||
import { getStandardPageName, convertPoints } from '../utils/helpers.js';
|
||||
|
||||
let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing
|
||||
|
||||
/**
|
||||
* 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').
|
||||
*/
|
||||
function renderTable(unit: any) {
|
||||
const tableBody = document.getElementById('dimensions-table-body');
|
||||
if (!tableBody) return;
|
||||
|
||||
tableBody.textContent = ''; // Clear the table body safely
|
||||
|
||||
analyzedPagesData.forEach(pageData => {
|
||||
const width = convertPoints(pageData.width, unit);
|
||||
const height = convertPoints(pageData.height, unit);
|
||||
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Create and append each cell safely using textContent
|
||||
const pageNumCell = document.createElement('td');
|
||||
pageNumCell.className = 'px-4 py-3 text-white';
|
||||
pageNumCell.textContent = pageData.pageNum;
|
||||
|
||||
const dimensionsCell = document.createElement('td');
|
||||
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
||||
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
||||
|
||||
const sizeCell = document.createElement('td');
|
||||
sizeCell.className = 'px-4 py-3 text-gray-300';
|
||||
sizeCell.textContent = pageData.standardSize;
|
||||
|
||||
const orientationCell = document.createElement('td');
|
||||
orientationCell.className = 'px-4 py-3 text-gray-300';
|
||||
orientationCell.textContent = pageData.orientation;
|
||||
|
||||
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to analyze the PDF and display dimensions.
|
||||
* This is called once after the file is loaded.
|
||||
*/
|
||||
export function analyzeAndDisplayDimensions() {
|
||||
if (!state.pdfDoc) return;
|
||||
|
||||
analyzedPagesData = []; // Reset stored data
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
pages.forEach((page: any, index: any) => {
|
||||
const { width, height } = page.getSize();
|
||||
analyzedPagesData.push({
|
||||
pageNum: index + 1,
|
||||
width, // Store raw width in points
|
||||
height, // Store raw height in points
|
||||
orientation: width > height ? 'Landscape' : 'Portrait',
|
||||
standardSize: getStandardPageName(width, height),
|
||||
});
|
||||
});
|
||||
|
||||
const resultsContainer = document.getElementById('dimensions-results');
|
||||
const unitsSelect = document.getElementById('units-select');
|
||||
|
||||
// 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
|
||||
renderTable(unitsSelect.value);
|
||||
|
||||
// Show the results table
|
||||
resultsContainer.classList.remove('hidden');
|
||||
|
||||
// Add event listener to handle unit changes
|
||||
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
|
||||
renderTable(e.target.value);
|
||||
});
|
||||
}
|
||||
93
src/js/logic/pdf-to-bmp.ts
Normal file
93
src/js/logic/pdf-to-bmp.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
/**
|
||||
* Creates a BMP file buffer from raw pixel data (ImageData).
|
||||
* This function is self-contained and has no external dependencies.
|
||||
* @param {ImageData} imageData The pixel data from a canvas context.
|
||||
* @returns {ArrayBuffer} The complete BMP file as an ArrayBuffer.
|
||||
*/
|
||||
function encodeBMP(imageData: any) {
|
||||
const { width, height, data } = imageData;
|
||||
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
|
||||
const fileSize = stride * height + 54; // 54 byte header
|
||||
const buffer = new ArrayBuffer(fileSize);
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// BMP File Header (14 bytes)
|
||||
view.setUint16(0, 0x4D42, true); // 'BM'
|
||||
view.setUint32(2, fileSize, true);
|
||||
view.setUint32(10, 54, true); // Offset to pixel data
|
||||
|
||||
// DIB Header (BITMAPINFOHEADER) (40 bytes)
|
||||
view.setUint32(14, 40, true); // DIB header size
|
||||
view.setUint32(18, width, true);
|
||||
view.setUint32(22, -height, true); // Negative height for top-down scanline order
|
||||
view.setUint16(26, 1, true); // Color planes
|
||||
view.setUint16(28, 24, true); // Bits per pixel
|
||||
view.setUint32(30, 0, true); // No compression
|
||||
view.setUint32(34, stride * height, true); // Image size
|
||||
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
|
||||
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
|
||||
|
||||
// Pixel Data
|
||||
let offset = 54;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const i = (y * width + x) * 4;
|
||||
// BMP is BGR, not RGB
|
||||
view.setUint8(offset++, data[i + 2]); // Blue
|
||||
view.setUint8(offset++, data[i + 1]); // Green
|
||||
view.setUint8(offset++, data[i]); // Red
|
||||
}
|
||||
// Add padding to make the row a multiple of 4 bytes
|
||||
for (let p = 0; p < (stride - width * 3); p++) {
|
||||
view.setUint8(offset++, 0);
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
export async function pdfToBmp() {
|
||||
showLoader('Converting PDF to BMP images...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
// Render the PDF page directly to the canvas
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
|
||||
// Get the raw pixel data from this canvas
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Use our new self-contained function to create the BMP file
|
||||
const bmpBuffer = encodeBMP(imageData);
|
||||
|
||||
// Add the generated BMP file to the zip archive
|
||||
zip.file(`page_${i}.bmp`, bmpBuffer);
|
||||
}
|
||||
|
||||
showLoader('Compressing files into a ZIP...');
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_bmp_images.zip');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to BMP. The file might be corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
58
src/js/logic/pdf-to-greyscale.ts
Normal file
58
src/js/logic/pdf-to-greyscale.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function pdfToGreyscale() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting to greyscale...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pdfBytes = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||
|
||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||
const page = await pdfjsDoc.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const data = imageData.data;
|
||||
for (let j = 0; j < data.length; j += 4) {
|
||||
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
|
||||
data[j] = avg; // red
|
||||
data[j + 1] = avg; // green
|
||||
data[j + 2] = avg; // blue
|
||||
}
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const imageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
||||
const reader = new FileReader();
|
||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||
reader.readAsArrayBuffer(blob);
|
||||
}, 'image/png'));
|
||||
|
||||
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
|
||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
||||
}
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'greyscale.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not convert to greyscale.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
35
src/js/logic/pdf-to-jpg.ts
Normal file
35
src/js/logic/pdf-to-jpg.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function pdfToJpg() {
|
||||
showLoader('Converting to JPG...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9));
|
||||
zip.file(`page_${i}.jpg`, blob as Blob);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_images.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to JPG. The file might be corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
31
src/js/logic/pdf-to-markdown.ts
Normal file
31
src/js/logic/pdf-to-markdown.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function pdfToMarkdown() {
|
||||
showLoader('Converting to Markdown...');
|
||||
try {
|
||||
const file = state.files[0];
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
let markdown = '';
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
// This is a simple text extraction. For more advanced formatting, more complex logic is needed.
|
||||
const text = content.items.map((item: any) => item.str).join(' ');
|
||||
markdown += text + '\n\n'; // Add double newline for paragraph breaks between pages
|
||||
}
|
||||
|
||||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||||
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Conversion Error', 'Failed to convert PDF. It may be image-based or corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
33
src/js/logic/pdf-to-png.ts
Normal file
33
src/js/logic/pdf-to-png.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
|
||||
export async function pdfToPng() {
|
||||
showLoader('Converting to PNG...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||||
zip.file(`page_${i}.png`, blob as Blob);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_pngs.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to PNG.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
38
src/js/logic/pdf-to-tiff.ts
Normal file
38
src/js/logic/pdf-to-tiff.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
import UTIF from 'utif';
|
||||
import * as pdfjsLib from "pdfjs-dist";
|
||||
|
||||
export async function pdfToTiff() {
|
||||
showLoader('Converting PDF to TIFF...');
|
||||
try {
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 }); // Use 2x scale for high quality
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const rgba = imageData.data;
|
||||
const tiffBuffer = UTIF.encodeImage(new Uint8Array(rgba), canvas.width, canvas.height);
|
||||
|
||||
zip.file(`page_${i}.tiff`, tiffBuffer);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_tiff_images.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to TIFF. The file might be corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
32
src/js/logic/pdf-to-webp.ts
Normal file
32
src/js/logic/pdf-to-webp.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
export async function pdfToWebp() {
|
||||
showLoader('Converting to WebP...');
|
||||
try {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
||||
const zip = new JSZip();
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
const context = canvas.getContext('2d');
|
||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 0.9));
|
||||
zip.file(`page_${i}.webp`, blob as Blob);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||
downloadFile(zipBlob, 'converted_webp.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert PDF to WebP.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
27
src/js/logic/pdf-to-zip.ts
Normal file
27
src/js/logic/pdf-to-zip.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
|
||||
export async function pdfToZip() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select one or more PDF files.');
|
||||
return;
|
||||
}
|
||||
showLoader('Creating ZIP file...');
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
for (const file of state.files) {
|
||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||
zip.file(file.name, fileBuffer as ArrayBuffer);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'pdfs.zip');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to create ZIP file.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
35
src/js/logic/png-to-pdf.ts
Normal file
35
src/js/logic/png-to-pdf.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function pngToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PNG file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Creating PDF from PNGs...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await readFileAsArrayBuffer(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'from_pngs.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to create PDF from PNG images. Ensure all files are valid PNGs.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
41
src/js/logic/redact.ts
Normal file
41
src/js/logic/redact.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'PDFLib' does not exist on type 'Window &... Remove this comment to see the full error message
|
||||
const { rgb } = window.PDFLib;
|
||||
|
||||
export async function redact(redactions: any, canvasScale: any) {
|
||||
showLoader('Applying redactions...');
|
||||
try {
|
||||
const pdfPages = state.pdfDoc.getPages();
|
||||
const conversionScale = 1 / canvasScale;
|
||||
|
||||
redactions.forEach((r: any) => {
|
||||
const page = pdfPages[r.pageIndex];
|
||||
const { height: pageHeight } = page.getSize();
|
||||
|
||||
// Convert canvas coordinates back to PDF coordinates
|
||||
const pdfX = r.canvasX * conversionScale;
|
||||
const pdfWidth = r.canvasWidth * conversionScale;
|
||||
const pdfHeight = r.canvasHeight * conversionScale;
|
||||
const pdfY = pageHeight - (r.canvasY * conversionScale) - pdfHeight;
|
||||
|
||||
page.drawRectangle({
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: pdfWidth,
|
||||
height: pdfHeight,
|
||||
color: rgb(0, 0, 0),
|
||||
});
|
||||
});
|
||||
|
||||
const redactedBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([redactedBytes], { type: 'application/pdf' }), 'redacted.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to apply redactions.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
104
src/js/logic/remove-annotations.ts
Normal file
104
src/js/logic/remove-annotations.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFName } from 'pdf-lib';
|
||||
|
||||
export function setupRemoveAnnotationsTool() {
|
||||
if (state.pdfDoc) {
|
||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount();
|
||||
}
|
||||
|
||||
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
|
||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||
pageScopeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
|
||||
});
|
||||
});
|
||||
|
||||
const selectAllCheckbox = document.getElementById('select-all-annotations');
|
||||
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
|
||||
selectAllCheckbox.addEventListener('change', () => {
|
||||
allAnnotCheckboxes.forEach(checkbox => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
checkbox.checked = selectAllCheckbox.checked;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeAnnotations() {
|
||||
showLoader('Removing annotations...');
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
let targetPageIndices = [];
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const pageScope = document.querySelector('input[name="page-scope"]:checked').value;
|
||||
if (pageScope === 'all') {
|
||||
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const rangeInput = document.getElementById('page-range-input').value;
|
||||
if (!rangeInput.trim()) throw new Error('Please enter a page range.');
|
||||
const ranges = rangeInput.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
targetPageIndices.push(pageNum - 1);
|
||||
}
|
||||
}
|
||||
targetPageIndices = [...new Set(targetPageIndices)];
|
||||
}
|
||||
|
||||
if (targetPageIndices.length === 0) throw new Error('No valid pages were selected.');
|
||||
|
||||
const typesToRemove = new Set(
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(cb => cb.value)
|
||||
);
|
||||
|
||||
if (typesToRemove.size === 0) throw new Error('Please select at least one annotation type to remove.');
|
||||
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
for (const pageIndex of targetPageIndices) {
|
||||
const page = pages[pageIndex];
|
||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||
|
||||
const annotsToKeep = [];
|
||||
|
||||
for (const ref of annotRefs) {
|
||||
const annot = state.pdfDoc.context.lookup(ref);
|
||||
const subtype = annot.get(PDFName.of('Subtype'))?.toString().substring(1);
|
||||
|
||||
// If the subtype is NOT in the list to remove, add it to our new array
|
||||
if (!subtype || !typesToRemove.has(subtype)) {
|
||||
annotsToKeep.push(ref);
|
||||
}
|
||||
}
|
||||
|
||||
if (annotsToKeep.length > 0) {
|
||||
const newAnnotsArray = state.pdfDoc.context.obj(annotsToKeep);
|
||||
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||
} else {
|
||||
page.node.delete(PDFName.of('Annots'));
|
||||
}
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'annotations-removed.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Could not remove annotations. Please check your page range.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
32
src/js/logic/remove-metadata.ts
Normal file
32
src/js/logic/remove-metadata.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function removeMetadata() {
|
||||
showLoader('Removing all metadata...');
|
||||
try {
|
||||
const infoDict = state.pdfDoc.getInfoDict();
|
||||
|
||||
const allKeys = infoDict.keys();
|
||||
allKeys.forEach((key: any) => {
|
||||
infoDict.delete(key);
|
||||
});
|
||||
|
||||
state.pdfDoc.setTitle('');
|
||||
state.pdfDoc.setAuthor('');
|
||||
state.pdfDoc.setSubject('');
|
||||
state.pdfDoc.setKeywords([]);
|
||||
state.pdfDoc.setCreator('');
|
||||
state.pdfDoc.setProducer('');
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-removed.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
26
src/js/logic/reverse-pages.ts
Normal file
26
src/js/logic/reverse-pages.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function reversePages() {
|
||||
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
|
||||
showLoader('Reversing page order...');
|
||||
try {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const pageCount = state.pdfDoc.getPageCount();
|
||||
const reversedIndices = Array.from({ length: pageCount }, (_, i) => pageCount - 1 - i);
|
||||
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
|
||||
const newPdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'reversed.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not reverse the PDF pages.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
30
src/js/logic/rotate.ts
Normal file
30
src/js/logic/rotate.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { degrees } from 'pdf-lib';
|
||||
|
||||
export async function rotate() {
|
||||
showLoader('Applying rotations...');
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndex = parseInt(item.dataset.pageIndex);
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const rotation = parseInt(item.dataset.rotation || '0');
|
||||
if (rotation !== 0) {
|
||||
const currentRotation = pages[pageIndex].getRotation().angle;
|
||||
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
||||
}
|
||||
});
|
||||
|
||||
const rotatedPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([rotatedPdfBytes], { type: 'application/pdf' }), 'rotated.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Could not apply rotations.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
4
src/js/logic/scan-to-pdf.ts
Normal file
4
src/js/logic/scan-to-pdf.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This is essentially the same as image-to-pdf.
|
||||
import { imageToPdf } from './image-to-pdf.js';
|
||||
|
||||
export const scanToPdf = imageToPdf;
|
||||
416
src/js/logic/sign-pdf.ts
Normal file
416
src/js/logic/sign-pdf.ts
Normal file
@@ -0,0 +1,416 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
const signState = {
|
||||
pdf: null, canvas: null, context: null, pageRendering: false,
|
||||
currentPageNum: 1, scale: 1.0,
|
||||
pageSnapshot: null,
|
||||
drawCanvas: null, drawContext: null, isDrawing: false,
|
||||
savedSignatures: [], placedSignatures: [], activeSignature: null,
|
||||
interactionMode: 'none',
|
||||
draggedSigId: null,
|
||||
dragOffsetX: 0, dragOffsetY: 0,
|
||||
hoveredSigId: null,
|
||||
resizeHandle: null,
|
||||
};
|
||||
|
||||
|
||||
async function renderPage(num: any) {
|
||||
signState.pageRendering = true;
|
||||
const page = await signState.pdf.getPage(num);
|
||||
const viewport = page.getViewport({ scale: signState.scale });
|
||||
signState.canvas.height = viewport.height;
|
||||
signState.canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: signState.context, viewport }).promise;
|
||||
|
||||
signState.pageSnapshot = signState.context.getImageData(0, 0, signState.canvas.width, signState.canvas.height);
|
||||
|
||||
drawSignatures();
|
||||
|
||||
signState.pageRendering = false;
|
||||
document.getElementById('current-page-display-sign').textContent = num;
|
||||
}
|
||||
|
||||
function drawSignatures() {
|
||||
if (!signState.pageSnapshot) return;
|
||||
signState.context.putImageData(signState.pageSnapshot, 0, 0);
|
||||
|
||||
signState.placedSignatures
|
||||
.filter(sig => sig.pageIndex === signState.currentPageNum - 1)
|
||||
.forEach(sig => {
|
||||
signState.context.drawImage(sig.image, sig.x, sig.y, sig.width, sig.height);
|
||||
|
||||
if (signState.hoveredSigId === sig.id || signState.draggedSigId === sig.id) {
|
||||
signState.context.strokeStyle = '#4f46e5';
|
||||
signState.context.setLineDash([6, 3]);
|
||||
signState.context.strokeRect(sig.x, sig.y, sig.width, sig.height);
|
||||
signState.context.setLineDash([]);
|
||||
|
||||
drawResizeHandles(sig);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function drawResizeHandles(sig: any) {
|
||||
const handleSize = 8;
|
||||
const halfHandle = handleSize / 2;
|
||||
const handles = getResizeHandles(sig);
|
||||
signState.context.fillStyle = '#4f46e5';
|
||||
Object.values(handles).forEach(handle => {
|
||||
signState.context.fillRect(handle.x - halfHandle, handle.y - halfHandle, handleSize, handleSize);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function fitToWidth() {
|
||||
const page = await signState.pdf.getPage(signState.currentPageNum);
|
||||
const container = document.getElementById('canvas-container-sign');
|
||||
signState.scale = container.clientWidth / page.getViewport({ scale: 1.0 }).width;
|
||||
renderPage(signState.currentPageNum);
|
||||
}
|
||||
|
||||
|
||||
function setupDrawingCanvas() {
|
||||
signState.drawCanvas = document.getElementById('signature-draw-canvas');
|
||||
signState.drawContext = signState.drawCanvas.getContext('2d');
|
||||
|
||||
const rect = signState.drawCanvas.getBoundingClientRect();
|
||||
const dpi = window.devicePixelRatio || 1;
|
||||
signState.drawCanvas.width = rect.width * dpi;
|
||||
signState.drawCanvas.height = rect.height * dpi;
|
||||
signState.drawContext.scale(dpi, dpi);
|
||||
signState.drawContext.lineWidth = 2;
|
||||
|
||||
const colorPicker = document.getElementById('signature-color');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
colorPicker.oninput = () => signState.drawContext.strokeStyle = colorPicker.value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
signState.drawContext.strokeStyle = colorPicker.value;
|
||||
|
||||
const start = (e: any) => {
|
||||
signState.isDrawing = true;
|
||||
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
||||
signState.drawContext.beginPath();
|
||||
signState.drawContext.moveTo(pos.x, pos.y);
|
||||
};
|
||||
const draw = (e: any) => {
|
||||
if (!signState.isDrawing) return;
|
||||
e.preventDefault();
|
||||
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
||||
signState.drawContext.lineTo(pos.x, pos.y);
|
||||
signState.drawContext.stroke();
|
||||
};
|
||||
const stop = () => signState.isDrawing = false;
|
||||
|
||||
['mousedown', 'touchstart'].forEach(evt => signState.drawCanvas.addEventListener(evt, start, { passive: false }));
|
||||
['mousemove', 'touchmove'].forEach(evt => signState.drawCanvas.addEventListener(evt, draw, { passive: false }));
|
||||
['mouseup', 'mouseleave', 'touchend'].forEach(evt => signState.drawCanvas.addEventListener(evt, stop));
|
||||
}
|
||||
|
||||
function getMousePos(canvas: any, evt: any) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return {
|
||||
x: evt.clientX - rect.left,
|
||||
y: evt.clientY - rect.top
|
||||
};
|
||||
}
|
||||
|
||||
function addSignatureToSaved(imageDataUrl: any) {
|
||||
const img = new Image();
|
||||
img.src = imageDataUrl;
|
||||
signState.savedSignatures.push(img);
|
||||
renderSavedSignatures();
|
||||
}
|
||||
|
||||
function renderSavedSignatures() {
|
||||
const container = document.getElementById('saved-signatures-container');
|
||||
container.textContent = ''; //change
|
||||
signState.savedSignatures.forEach((img, index) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'saved-signature p-1 bg-white rounded-md cursor-pointer border-2 border-transparent hover:border-indigo-500 h-16';
|
||||
img.className = 'h-full w-auto mx-auto';
|
||||
wrapper.appendChild(img);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
wrapper.onclick = () => {
|
||||
signState.activeSignature = { image: img, index };
|
||||
document.querySelectorAll('.saved-signature').forEach(el => el.classList.remove('selected'));
|
||||
wrapper.classList.add('selected');
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getResizeHandles(sig: any) {
|
||||
return {
|
||||
'top-left': { x: sig.x, y: sig.y },
|
||||
'top-middle': { x: sig.x + sig.width / 2, y: sig.y },
|
||||
'top-right': { x: sig.x + sig.width, y: sig.y },
|
||||
'middle-left': { x: sig.x, y: sig.y + sig.height / 2 },
|
||||
'middle-right': { x: sig.x + sig.width, y: sig.y + sig.height / 2 },
|
||||
'bottom-left': { x: sig.x, y: sig.y + sig.height },
|
||||
'bottom-middle':{ x: sig.x + sig.width / 2, y: sig.y + sig.height },
|
||||
'bottom-right': { x: sig.x + sig.width, y: sig.y + sig.height },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function getHandleAtPos(pos: any, sig: any) {
|
||||
const handles = getResizeHandles(sig);
|
||||
const handleSize = 10;
|
||||
for (const [name, handlePos] of Object.entries(handles)) {
|
||||
if (Math.abs(pos.x - handlePos.x) < handleSize && Math.abs(pos.y - handlePos.y) < handleSize) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setupPlacementListeners() {
|
||||
const canvas = signState.canvas;
|
||||
const ghost = document.getElementById('signature-ghost');
|
||||
|
||||
const mouseMoveHandler = (e: any) => {
|
||||
if (signState.interactionMode !== 'none') return;
|
||||
|
||||
if (signState.activeSignature) {
|
||||
ghost.style.backgroundImage = `url('${signState.activeSignature.image.src}')`;
|
||||
ghost.style.width = '150px';
|
||||
ghost.style.height = `${(signState.activeSignature.image.height / signState.activeSignature.image.width) * 150}px`;
|
||||
ghost.style.left = `${e.clientX + 5}px`;
|
||||
ghost.style.top = `${e.clientY + 5}px`;
|
||||
ghost.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const pos = getMousePos(canvas, e);
|
||||
let foundSigId: any = null;
|
||||
let foundHandle = null;
|
||||
|
||||
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
|
||||
if (foundSigId) return;
|
||||
const handle = getHandleAtPos(pos, sig);
|
||||
if (handle) {
|
||||
foundSigId = sig.id;
|
||||
foundHandle = handle;
|
||||
} else if (pos.x >= sig.x && pos.x <= sig.x + sig.width && pos.y >= sig.y && pos.y <= sig.y + sig.height) {
|
||||
foundSigId = sig.id;
|
||||
}
|
||||
});
|
||||
|
||||
canvas.className = '';
|
||||
if (foundHandle) {
|
||||
if (['top-left', 'bottom-right'].includes(foundHandle)) canvas.classList.add('resize-nwse');
|
||||
else if (['top-right', 'bottom-left'].includes(foundHandle)) canvas.classList.add('resize-nesw');
|
||||
else if (['top-middle', 'bottom-middle'].includes(foundHandle)) canvas.classList.add('resize-ns');
|
||||
else if (['middle-left', 'middle-right'].includes(foundHandle)) canvas.classList.add('resize-ew');
|
||||
} else if (foundSigId) {
|
||||
canvas.classList.add('movable');
|
||||
}
|
||||
|
||||
if (signState.hoveredSigId !== foundSigId) {
|
||||
signState.hoveredSigId = foundSigId;
|
||||
drawSignatures();
|
||||
}
|
||||
};
|
||||
|
||||
canvas.addEventListener('mousemove', mouseMoveHandler);
|
||||
document.getElementById('canvas-container-sign').addEventListener('mouseleave', () => ghost.classList.add('hidden'));
|
||||
|
||||
const onDragStart = (e: any) => {
|
||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||
let clickedOnSignature = false;
|
||||
|
||||
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
|
||||
if (clickedOnSignature) return;
|
||||
const handle = getHandleAtPos(pos, sig);
|
||||
if (handle) {
|
||||
signState.interactionMode = 'resize';
|
||||
signState.resizeHandle = handle;
|
||||
signState.draggedSigId = sig.id;
|
||||
clickedOnSignature = true;
|
||||
} else if (pos.x >= sig.x && pos.x <= sig.x + sig.width && pos.y >= sig.y && pos.y <= sig.y + sig.height) {
|
||||
signState.interactionMode = 'drag';
|
||||
signState.draggedSigId = sig.id;
|
||||
signState.dragOffsetX = pos.x - sig.x;
|
||||
signState.dragOffsetY = pos.y - sig.y;
|
||||
clickedOnSignature = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (clickedOnSignature) {
|
||||
ghost.classList.add('hidden');
|
||||
} else if (signState.activeSignature) {
|
||||
const sigWidth = 150;
|
||||
const sigHeight = (signState.activeSignature.image.height / signState.activeSignature.image.width) * sigWidth;
|
||||
signState.placedSignatures.push({
|
||||
id: Date.now(), image: signState.activeSignature.image,
|
||||
x: pos.x - sigWidth / 2, y: pos.y - sigHeight / 2,
|
||||
width: sigWidth, height: sigHeight, pageIndex: signState.currentPageNum - 1,
|
||||
aspectRatio: sigWidth / sigHeight,
|
||||
});
|
||||
drawSignatures();
|
||||
}
|
||||
};
|
||||
|
||||
const onDragMove = (e: any) => {
|
||||
if (signState.interactionMode === 'none') return;
|
||||
e.preventDefault();
|
||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||
const sig = signState.placedSignatures.find(s => s.id === signState.draggedSigId);
|
||||
if (!sig) return;
|
||||
|
||||
if (signState.interactionMode === 'drag') {
|
||||
sig.x = pos.x - signState.dragOffsetX;
|
||||
sig.y = pos.y - signState.dragOffsetY;
|
||||
} else if (signState.interactionMode === 'resize') {
|
||||
const originalRight = sig.x + sig.width;
|
||||
const originalBottom = sig.y + sig.height;
|
||||
|
||||
if (signState.resizeHandle.includes('right')) sig.width = Math.max(20, pos.x - sig.x);
|
||||
if (signState.resizeHandle.includes('bottom')) sig.height = Math.max(20, pos.y - sig.y);
|
||||
if (signState.resizeHandle.includes('left')) {
|
||||
sig.width = Math.max(20, originalRight - pos.x);
|
||||
sig.x = originalRight - sig.width;
|
||||
}
|
||||
if (signState.resizeHandle.includes('top')) {
|
||||
sig.height = Math.max(20, originalBottom - pos.y);
|
||||
sig.y = originalBottom - sig.height;
|
||||
}
|
||||
|
||||
if (signState.resizeHandle.includes('left') || signState.resizeHandle.includes('right')) {
|
||||
sig.height = sig.width / sig.aspectRatio;
|
||||
} else if (signState.resizeHandle.includes('top') || signState.resizeHandle.includes('bottom')) {
|
||||
sig.width = sig.height * sig.aspectRatio;
|
||||
}
|
||||
}
|
||||
drawSignatures();
|
||||
};
|
||||
|
||||
const onDragEnd = () => {
|
||||
signState.interactionMode = 'none';
|
||||
signState.draggedSigId = null;
|
||||
drawSignatures();
|
||||
};
|
||||
|
||||
['mousedown', 'touchstart'].forEach(evt => canvas.addEventListener(evt, onDragStart, { passive: false }));
|
||||
['mousemove', 'touchmove'].forEach(evt => canvas.addEventListener(evt, onDragMove, { passive: false }));
|
||||
['mouseup', 'mouseleave', 'touchend'].forEach(evt => canvas.addEventListener(evt, onDragEnd));
|
||||
}
|
||||
|
||||
export async function setupSignTool() {
|
||||
document.getElementById('signature-editor').classList.remove('hidden');
|
||||
|
||||
signState.canvas = document.getElementById('canvas-sign');
|
||||
signState.context = signState.canvas.getContext('2d');
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
signState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
document.getElementById('total-pages-display-sign').textContent = signState.pdf.numPages;
|
||||
|
||||
await fitToWidth();
|
||||
setupDrawingCanvas();
|
||||
setupPlacementListeners();
|
||||
|
||||
document.getElementById('prev-page-sign').onclick = () => { if (signState.currentPageNum > 1) { signState.currentPageNum--; renderPage(signState.currentPageNum); } };
|
||||
document.getElementById('next-page-sign').onclick = () => { if (signState.currentPageNum < signState.pdf.numPages) { signState.currentPageNum++; renderPage(signState.currentPageNum); } };
|
||||
document.getElementById('zoom-in-btn').onclick = () => { signState.scale += 0.25; renderPage(signState.currentPageNum); };
|
||||
document.getElementById('zoom-out-btn').onclick = () => { signState.scale = Math.max(0.25, signState.scale - 0.25); renderPage(signState.currentPageNum); };
|
||||
document.getElementById('fit-width-btn').onclick = fitToWidth;
|
||||
document.getElementById('undo-btn').onclick = () => { signState.placedSignatures.pop(); drawSignatures(); };
|
||||
|
||||
const tabs = ['draw', 'type', 'upload'];
|
||||
const tabButtons = tabs.map(t => document.getElementById(`${t}-tab-btn`));
|
||||
const tabPanels = tabs.map(t => document.getElementById(`${t}-panel`));
|
||||
tabButtons.forEach((button, index) => {
|
||||
button.onclick = () => {
|
||||
tabPanels.forEach(panel => panel.classList.add('hidden'));
|
||||
tabButtons.forEach(btn => {
|
||||
btn.classList.remove('border-indigo-500', 'text-white');
|
||||
btn.classList.add('border-transparent', 'text-gray-400');
|
||||
});
|
||||
tabPanels[index].classList.remove('hidden');
|
||||
button.classList.add('border-indigo-500', 'text-white');
|
||||
button.classList.remove('border-transparent', 'text-gray-400');
|
||||
};
|
||||
});
|
||||
|
||||
document.getElementById('clear-draw-btn').onclick = () => signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height);
|
||||
document.getElementById('save-draw-btn').onclick = () => { addSignatureToSaved(signState.drawCanvas.toDataURL()); signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height); };
|
||||
|
||||
const textInput = document.getElementById('signature-text-input');
|
||||
const fontPreview = document.getElementById('font-preview');
|
||||
const fontFamilySelect = document.getElementById('font-family-select');
|
||||
const fontSizeSlider = document.getElementById('font-size-slider');
|
||||
const fontSizeValue = document.getElementById('font-size-value');
|
||||
const fontColorPicker = document.getElementById('font-color-picker');
|
||||
|
||||
const updateFontPreview = () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.textContent = textInput.value || 'Your Name';
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.style.fontFamily = fontFamilySelect.value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.style.fontSize = `${fontSizeSlider.value}px`;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontPreview.style.color = fontColorPicker.value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
fontSizeValue.textContent = fontSizeSlider.value;
|
||||
};
|
||||
|
||||
[textInput, fontFamilySelect, fontSizeSlider, fontColorPicker].forEach(el => el.addEventListener('input', updateFontPreview));
|
||||
updateFontPreview();
|
||||
|
||||
document.getElementById('save-type-btn').onclick = async () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
if (!textInput.value) return;
|
||||
const canvas = await html2canvas(fontPreview, { backgroundColor: null, scale: 2 });
|
||||
addSignatureToSaved(canvas.toDataURL());
|
||||
};
|
||||
|
||||
document.getElementById('signature-upload-input').onchange = (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => addSignatureToSaved(event.target.result);
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
}
|
||||
|
||||
export async function applyAndSaveSignatures() {
|
||||
if (signState.placedSignatures.length === 0) {
|
||||
showAlert('No Signatures Placed', 'Please place at least one signature.');
|
||||
return;
|
||||
}
|
||||
showLoader('Applying signatures...');
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
for (const sig of signState.placedSignatures) {
|
||||
const page = pages[sig.pageIndex];
|
||||
const originalPageSize = page.getSize();
|
||||
const pngBytes = await fetch(sig.image.src).then(res => res.arrayBuffer());
|
||||
const pngImage = await state.pdfDoc.embedPng(pngBytes);
|
||||
|
||||
const renderedPage = await signState.pdf.getPage(sig.pageIndex + 1);
|
||||
const renderedViewport = renderedPage.getViewport({ scale: signState.scale });
|
||||
const scaleRatio = originalPageSize.width / renderedViewport.width;
|
||||
|
||||
page.drawImage(pngImage, {
|
||||
x: sig.x * scaleRatio,
|
||||
y: originalPageSize.height - (sig.y * scaleRatio) - (sig.height * scaleRatio),
|
||||
width: sig.width * scaleRatio,
|
||||
height: sig.height * scaleRatio,
|
||||
});
|
||||
}
|
||||
|
||||
const newPdfBytes = await state.pdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'signed.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to apply signatures.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
53
src/js/logic/split-in-half.ts
Normal file
53
src/js/logic/split-in-half.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||
|
||||
export async function splitInHalf() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const splitType = document.getElementById('split-type').value;
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'No PDF document is loaded.');
|
||||
return;
|
||||
}
|
||||
showLoader('Splitting PDF pages...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const pages = state.pdfDoc.getPages();
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const originalPage = pages[i];
|
||||
const { width, height } = originalPage.getSize();
|
||||
const whiteColor = rgb(1, 1, 1); // For masking
|
||||
|
||||
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
|
||||
|
||||
// Copy the page twice for all split types
|
||||
const [page1] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
const [page2] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||
|
||||
switch (splitType) {
|
||||
case 'vertical':
|
||||
page1.setCropBox(0, 0, width / 2, height);
|
||||
page2.setCropBox(width / 2, 0, width / 2, height);
|
||||
break;
|
||||
case 'horizontal':
|
||||
page1.setCropBox(0, height / 2, width, height / 2); // Top half
|
||||
page2.setCropBox(0, 0, width, height / 2); // Bottom half
|
||||
break;
|
||||
}
|
||||
newPdfDoc.addPage(page1);
|
||||
newPdfDoc.addPage(page2);
|
||||
}
|
||||
|
||||
const newPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'split-half.pdf');
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'An error occurred while splitting the PDF.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
213
src/js/logic/split.ts
Normal file
213
src/js/logic/split.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
// Track if visual selector has been rendered to avoid duplicates
|
||||
let visualSelectorRendered = false;
|
||||
|
||||
|
||||
async function renderVisualSelector() {
|
||||
if (visualSelectorRendered) return;
|
||||
|
||||
const container = document.getElementById('page-selector-grid');
|
||||
if (!container) return;
|
||||
|
||||
visualSelectorRendered = true;
|
||||
|
||||
container.textContent = '';
|
||||
|
||||
showLoader('Rendering page previews...');
|
||||
try {
|
||||
const pdfData = await state.pdfDoc.save();
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale: 0.4 });
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport }).promise;
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = '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'.
|
||||
wrapper.dataset.pageIndex = i - 1;
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL();
|
||||
img.className = 'rounded-md w-full h-auto';
|
||||
const p = document.createElement('p');
|
||||
p.className = 'text-center text-xs mt-1 text-gray-300';
|
||||
p.textContent = `Page ${i}`;
|
||||
wrapper.append(img, p);
|
||||
|
||||
const handleSelection = (e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isSelected = wrapper.classList.contains('selected');
|
||||
|
||||
if (isSelected) {
|
||||
wrapper.classList.remove('selected', 'border-indigo-500');
|
||||
wrapper.classList.add('border-transparent');
|
||||
} else {
|
||||
wrapper.classList.add('selected', 'border-indigo-500');
|
||||
wrapper.classList.remove('border-transparent');
|
||||
}
|
||||
};
|
||||
|
||||
wrapper.addEventListener('click', handleSelection);
|
||||
wrapper.addEventListener('touchend', handleSelection);
|
||||
|
||||
wrapper.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error rendering visual selector:', error);
|
||||
showAlert('Error', 'Failed to render page previews.');
|
||||
// 4. ADDED: Reset the flag on error so the user can try again.
|
||||
visualSelectorRendered = false;
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function setupSplitTool() {
|
||||
const splitModeSelect = document.getElementById('split-mode');
|
||||
const rangePanel = document.getElementById('range-panel');
|
||||
const visualPanel = document.getElementById('visual-select-panel');
|
||||
const evenOddPanel = document.getElementById('even-odd-panel');
|
||||
const zipOptionWrapper = document.getElementById('zip-option-wrapper');
|
||||
const allPagesPanel = document.getElementById('all-pages-panel');
|
||||
|
||||
if (!splitModeSelect) return;
|
||||
|
||||
splitModeSelect.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
|
||||
const mode = e.target.value;
|
||||
|
||||
if (mode !== 'visual') {
|
||||
visualSelectorRendered = false;
|
||||
const container = document.getElementById('page-selector-grid');
|
||||
if (container) container.innerHTML = '';
|
||||
}
|
||||
|
||||
rangePanel.classList.add('hidden');
|
||||
visualPanel.classList.add('hidden');
|
||||
evenOddPanel.classList.add('hidden');
|
||||
allPagesPanel.classList.add('hidden');
|
||||
zipOptionWrapper.classList.add('hidden');
|
||||
|
||||
if (mode === 'range') {
|
||||
rangePanel.classList.remove('hidden');
|
||||
zipOptionWrapper.classList.remove('hidden');
|
||||
} else if (mode === 'visual') {
|
||||
visualPanel.classList.remove('hidden');
|
||||
zipOptionWrapper.classList.remove('hidden');
|
||||
renderVisualSelector();
|
||||
} else if (mode === 'even-odd') {
|
||||
evenOddPanel.classList.remove('hidden');
|
||||
} else if (mode === 'all') {
|
||||
allPagesPanel.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function split() {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const splitMode = document.getElementById('split-mode').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||
const downloadAsZip = document.getElementById('download-as-zip')?.checked || false;
|
||||
|
||||
showLoader('Splitting PDF...');
|
||||
|
||||
try {
|
||||
const totalPages = state.pdfDoc.getPageCount();
|
||||
let indicesToExtract: any = [];
|
||||
|
||||
switch (splitMode) {
|
||||
case 'range':
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageRangeInput = document.getElementById('page-range').value;
|
||||
if (!pageRangeInput) throw new Error('Please enter a page range.');
|
||||
const ranges = pageRangeInput.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
||||
for (let i = start; i <= end; i++) indicesToExtract.push(i - 1);
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indicesToExtract.push(pageNum - 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'even-odd':
|
||||
const choiceElement = document.querySelector('input[name="even-odd-choice"]:checked');
|
||||
if (!choiceElement) throw new Error('Please select even or odd pages.');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||
const choice = choiceElement.value;
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i);
|
||||
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
||||
}
|
||||
break;
|
||||
case 'all':
|
||||
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
||||
break;
|
||||
case 'visual':
|
||||
indicesToExtract = Array.from(document.querySelectorAll('.page-thumbnail-wrapper.selected'))
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
.map(el => parseInt(el.dataset.pageIndex));
|
||||
break;
|
||||
}
|
||||
|
||||
const uniqueIndices = [...new Set(indicesToExtract)];
|
||||
if (uniqueIndices.length === 0) {
|
||||
throw new Error('No pages were selected for splitting.');
|
||||
}
|
||||
|
||||
if (splitMode === 'all' || (['range', 'visual'].includes(splitMode) && downloadAsZip)) {
|
||||
showLoader('Creating ZIP file...');
|
||||
const zip = new JSZip();
|
||||
for (const index of uniqueIndices) {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
|
||||
newPdf.addPage(copiedPage);
|
||||
const pdfBytes = await newPdf.save();
|
||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||
zip.file(`page-${index + 1}.pdf`, pdfBytes);
|
||||
}
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
downloadFile(zipBlob, 'split-pages.zip');
|
||||
} else {
|
||||
const newPdf = await PDFLibDocument.create();
|
||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, uniqueIndices as number[]);
|
||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||
const pdfBytes = await newPdf.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'split-document.pdf');
|
||||
}
|
||||
|
||||
if (splitMode === 'visual') {
|
||||
visualSelectorRendered = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', e.message || 'Failed to split PDF. Please check your selection.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
55
src/js/logic/svg-to-pdf.ts
Normal file
55
src/js/logic/svg-to-pdf.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
async function convertImageToPngBytes(file: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
img.onload = async () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
resolve(pngBytes);
|
||||
};
|
||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
||||
img.src = e.target.result;
|
||||
};
|
||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
export async function svgToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one SVG file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting SVG to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const pngBytes = await convertImageToPngBytes(file);
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'from_svgs.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert SVG to PDF. One of the files may be invalid.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
82
src/js/logic/tiff-to-pdf.ts
Normal file
82
src/js/logic/tiff-to-pdf.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import { decode } from 'tiff';
|
||||
|
||||
export async function tiffToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one TIFF file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting TIFF to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const tiffBytes = await readFileAsArrayBuffer(file);
|
||||
const ifds = decode(tiffBytes as any);
|
||||
|
||||
for (const ifd of ifds) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = ifd.width;
|
||||
canvas.height = ifd.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
throw new Error('Failed to get canvas context');
|
||||
}
|
||||
|
||||
const imageData = ctx.createImageData(ifd.width, ifd.height);
|
||||
const pixels = imageData.data;
|
||||
|
||||
// Calculate samples per pixel from data length
|
||||
const totalPixels = ifd.width * ifd.height;
|
||||
const samplesPerPixel = ifd.data.length / totalPixels;
|
||||
|
||||
// Convert TIFF data to RGBA
|
||||
for (let i = 0; i < totalPixels; i++) {
|
||||
const dstIndex = i * 4;
|
||||
|
||||
if (samplesPerPixel === 1) {
|
||||
// Grayscale
|
||||
const gray = ifd.data[i];
|
||||
pixels[dstIndex] = gray;
|
||||
pixels[dstIndex + 1] = gray;
|
||||
pixels[dstIndex + 2] = gray;
|
||||
pixels[dstIndex + 3] = 255;
|
||||
} else if (samplesPerPixel === 3) {
|
||||
// RGB
|
||||
const srcIndex = i * 3;
|
||||
pixels[dstIndex] = ifd.data[srcIndex];
|
||||
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
||||
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
||||
pixels[dstIndex + 3] = 255;
|
||||
} else if (samplesPerPixel === 4) {
|
||||
// RGBA
|
||||
const srcIndex = i * 4;
|
||||
pixels[dstIndex] = ifd.data[srcIndex];
|
||||
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
||||
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
||||
pixels[dstIndex + 3] = ifd.data[srcIndex + 3];
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
const pngBlob = await new Promise<Blob>((res) => canvas.toBlob(res!, 'image/png'));
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
||||
}
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'from_tiff.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
74
src/js/logic/txt-to-pdf.ts
Normal file
74
src/js/logic/txt-to-pdf.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts, PageSizes } from 'pdf-lib';
|
||||
|
||||
export async function txtToPdf() {
|
||||
showLoader('Creating PDF...');
|
||||
try {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const text = document.getElementById('text-input').value;
|
||||
if (!text.trim()) {
|
||||
showAlert('Input Required', 'Please enter some text to convert.');
|
||||
hideLoader();
|
||||
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 fontFamilyKey = document.getElementById('font-family').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const pageSizeKey = document.getElementById('page-size').value;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const colorHex = document.getElementById('text-color').value;
|
||||
const textColor = hexToRgb(colorHex);
|
||||
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]);
|
||||
const pageSize = PageSizes[pageSizeKey];
|
||||
const margin = 72; // 1 inch
|
||||
|
||||
let page = pdfDoc.addPage(pageSize);
|
||||
let { width, height } = page.getSize();
|
||||
const textWidth = width - margin * 2;
|
||||
const lineHeight = fontSize * 1.3;
|
||||
let y = height - margin;
|
||||
|
||||
const paragraphs = text.split('\n');
|
||||
for (const paragraph of paragraphs) {
|
||||
const words = paragraph.split(' ');
|
||||
let currentLine = '';
|
||||
for (const word of words) {
|
||||
const testLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
||||
if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) {
|
||||
currentLine = testLine;
|
||||
} else {
|
||||
if (y < margin + lineHeight) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
y = page.getHeight() - margin;
|
||||
}
|
||||
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
|
||||
y -= lineHeight;
|
||||
currentLine = word;
|
||||
}
|
||||
}
|
||||
if (currentLine.length > 0) {
|
||||
if (y < margin + lineHeight) {
|
||||
page = pdfDoc.addPage(pageSize);
|
||||
y = page.getHeight() - margin;
|
||||
}
|
||||
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
|
||||
y -= lineHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'text-document.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to create PDF from text.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
4
src/js/logic/view-metadata.ts
Normal file
4
src/js/logic/view-metadata.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// This tool doesn't have a "process" button. Its logic is handled directly in fileHandler.js after a file is uploaded.
|
||||
export function viewMetadata() {
|
||||
console.log("");
|
||||
}
|
||||
48
src/js/logic/webp-to-pdf.ts
Normal file
48
src/js/logic/webp-to-pdf.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
|
||||
export async function webpToPdf() {
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one WebP file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Converting WebP to PDF...');
|
||||
try {
|
||||
const pdfDoc = await PDFLibDocument.create();
|
||||
for (const file of state.files) {
|
||||
const webpBytes = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2322) FIXME: Type 'unknown' is not assignable to type 'BlobPart... Remove this comment to see the full error message
|
||||
const imageBitmap = await createImageBitmap(new Blob([webpBytes]));
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = imageBitmap.width;
|
||||
canvas.height = imageBitmap.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(imageBitmap, 0, 0);
|
||||
|
||||
const pngBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||
const pngBytes = await pngBlob.arrayBuffer();
|
||||
|
||||
// Embed the converted PNG into the PDF
|
||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||
page.drawImage(pngImage, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pngImage.width,
|
||||
height: pngImage.height,
|
||||
});
|
||||
}
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
downloadFile(new Blob([pdfBytes], { type: 'application/pdf' }), 'from_webp.pdf');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showAlert('Error', 'Failed to convert WebP to PDF. Ensure all files are valid WebP images.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
131
src/js/logic/word-to-pdf.ts
Normal file
131
src/js/logic/word-to-pdf.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// NOTE: This is a work in progress and does not work correctly as of yet
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export async function wordToPdf() {
|
||||
const file = state.files[0];
|
||||
if (!file) {
|
||||
showAlert('No File', 'Please upload a .docx file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoader('Preparing preview...');
|
||||
|
||||
try {
|
||||
|
||||
const mammothOptions = {
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||
convertImage: mammoth.images.inline((element: any) => {
|
||||
return element.read("base64").then((imageBuffer: any) => {
|
||||
return {
|
||||
src: `data:${element.contentType};base64,${imageBuffer}`
|
||||
};
|
||||
});
|
||||
})
|
||||
};
|
||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||
const { value: html } = await mammoth.convertToHtml({ arrayBuffer }, mammothOptions);
|
||||
|
||||
// Get references to our modal elements from index.html
|
||||
const previewModal = document.getElementById('preview-modal');
|
||||
const previewContent = document.getElementById('preview-content');
|
||||
const downloadBtn = document.getElementById('preview-download-btn');
|
||||
const closeBtn = document.getElementById('preview-close-btn');
|
||||
|
||||
const styledHtml = `
|
||||
<style>
|
||||
#preview-content { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: black; }
|
||||
#preview-content table { border-collapse: collapse; width: 100%; }
|
||||
#preview-content td, #preview-content th { border: 1px solid #dddddd; text-align: left; padding: 8px; }
|
||||
#preview-content img { max-width: 100%; height: auto; }
|
||||
#preview-content a { color: #0000ee; text-decoration: underline; }
|
||||
</style>
|
||||
${html}
|
||||
`;
|
||||
previewContent.innerHTML = styledHtml;
|
||||
|
||||
const marginDiv = document.createElement('div');
|
||||
marginDiv.style.height = '100px';
|
||||
previewContent.appendChild(marginDiv);
|
||||
|
||||
const images = previewContent.querySelectorAll('img');
|
||||
const imagePromises = Array.from(images).map(img => {
|
||||
return new Promise((resolve) => {
|
||||
// @ts-expect-error TS(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
|
||||
if (img.complete) resolve();
|
||||
else img.onload = resolve;
|
||||
});
|
||||
});
|
||||
await Promise.all(imagePromises);
|
||||
|
||||
|
||||
previewModal.classList.remove('hidden');
|
||||
hideLoader();
|
||||
|
||||
const downloadHandler = async () => {
|
||||
showLoader('Generating High-Quality PDF...');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||
const { jsPDF } = window.jspdf;
|
||||
const doc = new jsPDF({
|
||||
orientation: 'p',
|
||||
unit: 'pt',
|
||||
format: 'letter'
|
||||
});
|
||||
|
||||
await doc.html(previewContent, {
|
||||
callback: function (doc: any) {
|
||||
const links = previewContent.querySelectorAll('a');
|
||||
const pageHeight = doc.internal.pageSize.getHeight();
|
||||
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
|
||||
|
||||
links.forEach(link => {
|
||||
if (!link.href) return;
|
||||
|
||||
const linkRect = link.getBoundingClientRect();
|
||||
|
||||
// Calculate position relative to the preview container's top-left
|
||||
const relativeX = linkRect.left - containerRect.left;
|
||||
const relativeY = linkRect.top - containerRect.top;
|
||||
|
||||
const pageNum = Math.floor(relativeY / pageHeight) + 1;
|
||||
const yOnPage = relativeY % pageHeight;
|
||||
|
||||
doc.setPage(pageNum);
|
||||
try {
|
||||
doc.link(relativeX + 45, yOnPage + 45, linkRect.width, linkRect.height, { url: link.href });
|
||||
} catch (e) {
|
||||
console.warn("Could not add link:", link.href, e);
|
||||
}
|
||||
});
|
||||
|
||||
const outputFileName = `${file.name.replace(/\.[^/.]+$/, "")}.pdf`;
|
||||
doc.save(outputFileName);
|
||||
hideLoader();
|
||||
},
|
||||
autoPaging: 'slice',
|
||||
x: 45,
|
||||
y: 45,
|
||||
width: 522,
|
||||
windowWidth: previewContent.scrollWidth
|
||||
});
|
||||
};
|
||||
|
||||
const closeHandler = () => {
|
||||
previewModal.classList.add('hidden');
|
||||
previewContent.innerHTML = '';
|
||||
downloadBtn.removeEventListener('click', downloadHandler);
|
||||
closeBtn.removeEventListener('click', closeHandler);
|
||||
};
|
||||
|
||||
downloadBtn.addEventListener('click', downloadHandler);
|
||||
closeBtn.addEventListener('click', closeHandler);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
hideLoader();
|
||||
showAlert('Preview Error', `Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`);
|
||||
}
|
||||
}
|
||||
119
src/js/main.ts
Normal file
119
src/js/main.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { categories } from './config/tools.js';
|
||||
import { dom, switchView, hideAlert } from './ui.js';
|
||||
import { setupToolInterface } from './handlers/toolSelectionHandler.js';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
|
||||
const init = () => {
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
dom.toolGrid.textContent = '';
|
||||
|
||||
categories.forEach(category => {
|
||||
const categoryGroup = document.createElement('div');
|
||||
categoryGroup.className = 'category-group col-span-full';
|
||||
|
||||
const title = document.createElement('h2');
|
||||
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0';
|
||||
title.textContent = category.name;
|
||||
|
||||
const toolsContainer = document.createElement('div');
|
||||
toolsContainer.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
|
||||
|
||||
category.tools.forEach(tool => {
|
||||
const toolCard = document.createElement('div');
|
||||
toolCard.className = 'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center';
|
||||
toolCard.dataset.toolId = tool.id;
|
||||
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
|
||||
icon.setAttribute('data-lucide', tool.icon);
|
||||
|
||||
const toolName = document.createElement('h3');
|
||||
toolName.className = 'font-semibold text-white';
|
||||
toolName.textContent = tool.name;
|
||||
|
||||
toolCard.append(icon, toolName);
|
||||
|
||||
if (tool.subtitle) {
|
||||
const toolSubtitle = document.createElement('p');
|
||||
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
|
||||
toolSubtitle.textContent = tool.subtitle;
|
||||
toolCard.appendChild(toolSubtitle);
|
||||
}
|
||||
|
||||
toolsContainer.appendChild(toolCard);
|
||||
});
|
||||
|
||||
categoryGroup.append(title, toolsContainer);
|
||||
dom.toolGrid.appendChild(categoryGroup);
|
||||
});
|
||||
|
||||
const searchBar = document.getElementById('search-bar');
|
||||
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
|
||||
|
||||
searchBar.addEventListener('input', () => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const searchTerm = searchBar.value.toLowerCase().trim();
|
||||
|
||||
categoryGroups.forEach(group => {
|
||||
const toolCards = group.querySelectorAll('.tool-card');
|
||||
let visibleToolsInCategory = 0;
|
||||
|
||||
toolCards.forEach(card => {
|
||||
const toolName = card.querySelector('h3').textContent.toLowerCase();
|
||||
const toolSubtitle = card.querySelector('p')?.textContent.toLowerCase() || '';
|
||||
const isMatch = toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
|
||||
|
||||
card.classList.toggle('hidden', !isMatch);
|
||||
if (isMatch) {
|
||||
visibleToolsInCategory++;
|
||||
}
|
||||
});
|
||||
|
||||
group.classList.toggle('hidden', visibleToolsInCategory === 0);
|
||||
});
|
||||
});
|
||||
|
||||
dom.toolGrid.addEventListener('click', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
|
||||
const card = e.target.closest('.tool-card');
|
||||
if (card) {
|
||||
const toolId = card.dataset.toolId;
|
||||
setupToolInterface(toolId);
|
||||
}
|
||||
});
|
||||
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
|
||||
dom.alertOkBtn.addEventListener('click', hideAlert);
|
||||
|
||||
const faqAccordion = document.getElementById('faq-accordion');
|
||||
if (faqAccordion) {
|
||||
faqAccordion.addEventListener('click', (e) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
|
||||
const questionButton = e.target.closest('.faq-question');
|
||||
if (!questionButton) return;
|
||||
|
||||
const faqItem = questionButton.parentElement;
|
||||
const answer = faqItem.querySelector('.faq-answer');
|
||||
|
||||
faqItem.classList.toggle('open');
|
||||
|
||||
if (faqItem.classList.contains('open')) {
|
||||
answer.style.maxHeight = answer.scrollHeight + 'px';
|
||||
} else {
|
||||
answer.style.maxHeight = '0px';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createIcons({ icons });
|
||||
console.log('Please share our tool and share the love!');
|
||||
};
|
||||
|
||||
// --- START THE APP ---
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
16
src/js/state.ts
Normal file
16
src/js/state.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const state = {
|
||||
activeTool: null,
|
||||
files: [],
|
||||
pdfDoc: null,
|
||||
pdfPages: [],
|
||||
currentPdfUrl: null,
|
||||
};
|
||||
|
||||
// Resets the state when switching views or completing an operation.
|
||||
export function resetState() {
|
||||
state.activeTool = null;
|
||||
state.files = [];
|
||||
state.pdfDoc = null;
|
||||
state.pdfPages = [];
|
||||
document.getElementById('tool-content').innerHTML = '';
|
||||
}
|
||||
1698
src/js/ui.ts
Normal file
1698
src/js/ui.ts
Normal file
File diff suppressed because it is too large
Load Diff
113
src/js/utils/helpers.ts
Normal file
113
src/js/utils/helpers.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
const STANDARD_SIZES = {
|
||||
'A4': { width: 595.28, height: 841.89 },
|
||||
'Letter': { width: 612, height: 792 },
|
||||
'Legal': { width: 612, height: 1008 },
|
||||
'Tabloid': { width: 792, height: 1224 },
|
||||
'A3': { width: 841.89, height: 1190.55 },
|
||||
'A5': { width: 419.53, height: 595.28 },
|
||||
};
|
||||
|
||||
export function getStandardPageName(width: any, height: any) {
|
||||
const tolerance = 1; // Allow for minor floating point variations
|
||||
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
|
||||
if ((Math.abs(width - size.width) < tolerance && Math.abs(height - size.height) < tolerance) ||
|
||||
(Math.abs(width - size.height) < tolerance && Math.abs(height - size.width) < tolerance)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return 'Custom';
|
||||
}
|
||||
|
||||
export function convertPoints(points: any, unit: any) {
|
||||
let result = 0;
|
||||
switch (unit) {
|
||||
case 'in':
|
||||
result = points / 72;
|
||||
break;
|
||||
case 'mm':
|
||||
result = (points / 72) * 25.4;
|
||||
break;
|
||||
case 'px':
|
||||
result = points * (96 / 72); // Assuming 96 DPI
|
||||
break;
|
||||
default: // 'pt'
|
||||
result = points;
|
||||
break;
|
||||
}
|
||||
return result.toFixed(2);
|
||||
}
|
||||
|
||||
export const hexToRgb = (hex: any) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16) / 255,
|
||||
g: parseInt(result[2], 16) / 255,
|
||||
b: parseInt(result[3], 16) / 255,
|
||||
} : { r: 0, g: 0, b: 0 }; // Default to black
|
||||
};
|
||||
|
||||
export const formatBytes = (bytes: any, decimals = 1) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
export const downloadFile = (blob: any, filename: any) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
export const readFileAsArrayBuffer = (file: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
};
|
||||
|
||||
export function parsePageRanges(rangeString: any, totalPages: any) {
|
||||
if (!rangeString || rangeString.trim() === '') {
|
||||
return Array.from({ length: totalPages }, (_, i) => i);
|
||||
}
|
||||
|
||||
const indices = new Set();
|
||||
const parts = rangeString.split(',');
|
||||
|
||||
for (const part of parts) {
|
||||
const trimmedPart = part.trim();
|
||||
if (!trimmedPart) continue;
|
||||
|
||||
if (trimmedPart.includes('-')) {
|
||||
const [start, end] = trimmedPart.split('-').map(Number);
|
||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) {
|
||||
console.warn(`Invalid range skipped: ${trimmedPart}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
indices.add(i - 1);
|
||||
}
|
||||
} else {
|
||||
const pageNum = Number(trimmedPart);
|
||||
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
|
||||
console.warn(`Invalid page number skipped: ${trimmedPart}`);
|
||||
continue;
|
||||
}
|
||||
indices.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
return Array.from(indices).sort((a, b) => a - b);
|
||||
}
|
||||
140
terms.html
Normal file
140
terms.html
Normal file
@@ -0,0 +1,140 @@
|
||||
<!DOCTYPE html>
|
||||
<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>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="dist/styles.css">
|
||||
<link rel="icon" type="image/png" href="images/favicon.svg">
|
||||
<style>
|
||||
.legal-content h2 { @apply text-2xl font-bold text-white mt-8 mb-4; }
|
||||
.legal-content h3 { @apply text-xl font-semibold text-indigo-400 mt-6 mb-3; }
|
||||
.legal-content p { @apply mb-4 leading-relaxed text-gray-400; }
|
||||
.legal-content ul { @apply list-disc list-inside mb-4 pl-4 text-gray-400; }
|
||||
.legal-content a { @apply text-indigo-400 underline hover:text-indigo-300; }
|
||||
</style>
|
||||
</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">
|
||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
|
||||
<img src="images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8 mr-2">
|
||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
||||
</a>
|
||||
<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="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
|
||||
<div class="md:hidden flex items-center gap-4">
|
||||
<a href="index.html" class="nav-link">Home</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
</div>
|
||||
</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:contact@bentopdf.com">contact@bentopdf.com</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="BentoPDF 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://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="instagram"></i>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
||||
<i data-lucide="linkedin"></i>
|
||||
</a>
|
||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
||||
<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>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
61
tsconfig.json
Normal file
61
tsconfig.json
Normal file
@@ -0,0 +1,61 @@
|
||||
// {
|
||||
// "compilerOptions": {
|
||||
// "target": "ES2022",
|
||||
// "useDefineForClassFields": true,
|
||||
// "module": "ESNext",
|
||||
// "lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
// "types": ["vite/client"],
|
||||
// "skipLibCheck": true,
|
||||
|
||||
// /* Bundler mode */
|
||||
// "moduleResolution": "bundler",
|
||||
// "allowImportingTsExtensions": true,
|
||||
// "verbatimModuleSyntax": true,
|
||||
// "moduleDetection": "force",
|
||||
// "noEmit": true,
|
||||
|
||||
// /* Linting */
|
||||
// "strict": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "erasableSyntaxOnly": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
// "noUncheckedSideEffectImports": true
|
||||
// },
|
||||
// "include": ["src"]
|
||||
// }
|
||||
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
|
||||
/* Easier module handling for Vite */
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
/* Disable strict checks for now */
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"noUncheckedSideEffectImports": false,
|
||||
|
||||
/* Fix for ArrayBuffer type issues */
|
||||
"noImplicitAny": false,
|
||||
|
||||
/* Quality-of-life options */
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
26
vite.config.ts
Normal file
26
vite.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
nodePolyfills({
|
||||
include: ['buffer', 'stream', 'util', 'zlib', 'process'],
|
||||
globals: {
|
||||
Buffer: true,
|
||||
global: true,
|
||||
process: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
stream: 'stream-browserify',
|
||||
zlib: 'browserify-zlib',
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['pdfkit', 'blob-stream'],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user