mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-12 01:33:36 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f213d9dc27 | ||
|
|
b39d2c267c | ||
|
|
09436a689d | ||
|
|
c5764e4172 | ||
|
|
2741b29ad1 | ||
|
|
05bc0d5a3c | ||
|
|
8651ca366f | ||
|
|
6b7163991e | ||
|
|
f3f6af079f | ||
|
|
6e7563d0c2 | ||
|
|
41a35586c7 |
No files matched your search
+4
-1
@@ -13,7 +13,10 @@
|
|||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div 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" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<span class="text-white font-bold text-xl ml-2">
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
<a href="index.html">BentoPDF</a>
|
<a href="index.html">BentoPDF</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+4
-1
@@ -13,7 +13,10 @@
|
|||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div 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" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<span class="text-white font-bold text-xl ml-2">
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
<a href="index.html">BentoPDF</a>
|
<a href="index.html">BentoPDF</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -13,7 +13,10 @@
|
|||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div 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" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<span class="text-white font-bold text-xl ml-2">
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
<a href="index.html">BentoPDF</a>
|
<a href="index.html">BentoPDF</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+114
-28
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>BentoPDF - The Privacy First PDF Toolkit</title>
|
<title>BentoPDF - The Privacy First PDF Toolkit</title>
|
||||||
<link rel="icon" type="image/png" href="./images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -27,10 +30,38 @@
|
|||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
<a href="./licensing.html" class="nav-link">Licensing</a>
|
<a href="./licensing.html" class="nav-link">Licensing</a>
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
|
<a href="https://github.com/alam00000/bentopdf/" target="_blank" rel="noopener noreferrer" class="
|
||||||
|
inline-flex items-center gap-1.5 text-sm font-medium
|
||||||
|
bg-gray-800 text-gray-200 border border-gray-600
|
||||||
|
pl-2.5 pr-3 py-1
|
||||||
|
rounded-full transition-colors duration-200
|
||||||
|
shadow-sm hover:shadow-md hover:bg-gray-700
|
||||||
|
">
|
||||||
|
<svg class="w-4 h-4 flex-shrink-0 text-gray-200" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span id="github-stars-desktop">-</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Mobile Hamburger Button -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="md:hidden flex items-center gap-4">
|
||||||
|
<a href="https://github.com/alam00000/bentopdf/" target="_blank" rel="noopener noreferrer" class="
|
||||||
|
inline-flex items-center gap-1.5 text-sm font-medium
|
||||||
|
bg-gray-800 text-gray-200 border border-gray-600
|
||||||
|
pl-2.5 pr-3 py-1
|
||||||
|
rounded-full transition-colors duration-200
|
||||||
|
shadow-sm hover:shadow-md hover:bg-gray-700
|
||||||
|
">
|
||||||
|
<svg class="w-4 h-4 flex-shrink-0 text-gray-200" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span id="github-stars-mobile">-</span>
|
||||||
|
</a>
|
||||||
<button id="mobile-menu-button" type="button"
|
<button id="mobile-menu-button" type="button"
|
||||||
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
aria-controls="mobile-menu" aria-expanded="false">
|
aria-controls="mobile-menu" aria-expanded="false">
|
||||||
@@ -62,52 +93,106 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
<section id="hero-section" class="text-center py-20">
|
<section id="hero-section" class="text-center py-20 relative">
|
||||||
|
<div class="bg-grid-pattern"></div>
|
||||||
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
|
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
|
||||||
The <span class="marker-slanted"> PDF Toolkit </span> built for
|
The <span
|
||||||
|
class="inline-flex items-center gap-3 border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 align-middle">
|
||||||
|
PDF Toolkit <i data-lucide="briefcase" class="w-8 h-8 md:w-12 md:h-12 transform -rotate-4"></i> </span> built
|
||||||
|
for
|
||||||
privacy<span
|
privacy<span
|
||||||
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
|
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p>
|
<!-- <p class="text-lg text-gray-400 mb-12">Fast, Secure and Forever Free.</p> -->
|
||||||
<div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8">
|
<div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-12 mt-12">
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
|
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium">
|
||||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
<i data-lucide="check-circle" class="w-4 h-4 text-gray-400"></i>
|
||||||
No Signups
|
No Signups
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
|
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium">
|
||||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
<i data-lucide="check-circle" class="w-4 h-4 text-gray-400"></i>
|
||||||
Unlimited Use
|
Unlimited Use
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
|
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium">
|
||||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
<i data-lucide="check-circle" class="w-4 h-4 text-gray-400"></i>
|
||||||
Works Offline
|
Works Offline
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<a href="#tools-header"
|
<a href="#tools-header"
|
||||||
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">Start
|
class="inline-flex items-center gap-3 pl-8 pr-2 py-2 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">
|
||||||
Using - Forever Free</a>
|
Start Using Now
|
||||||
<a href="https://github.com/alam00000/bentopdf/stargazers" target="_blank" rel="noopener noreferrer" class="
|
<span class="bg-white text-indigo-600 rounded-full p-2">
|
||||||
inline-flex items-center gap-1.5 text-sm font-medium
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||||
bg-white text-gray-800 border border-gray-300
|
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||||
dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600
|
<path d="M5 12h14" />
|
||||||
pl-2.5 pr-3 py-1
|
<path d="m12 5 7 7-7 7" />
|
||||||
rounded-full transition-colors duration-200
|
</svg>
|
||||||
shadow-sm hover:shadow-md hover:bg-gray-50 dark:hover:bg-gray-700
|
</span>
|
||||||
">
|
</a>
|
||||||
<svg class="w-4 h-4 flex-shrink-0 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true">
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- USED BY SECTION START -->
|
||||||
|
<section class="py-10 hide-section">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<p class="text-center text-gray-500 text-sm font-medium mb-8">Used by companies and people working at</p>
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap justify-center items-center gap-8 md:gap-12 opacity-70 grayscale hover:grayscale-0 transition-all duration-500">
|
||||||
|
|
||||||
|
<!-- Google -->
|
||||||
|
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 24 24"
|
||||||
|
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .533 5.347.533 12S5.867 24 12.48 24c3.44 0 6.013-1.133 8.053-3.227 2.107-2.08 2.72-5.027 2.72-7.44 0-.733-.053-1.44-.16-2.133h-10.613z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Microsoft -->
|
||||||
|
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 24 24"
|
||||||
|
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M0 0h11.377v11.372H0zM12.623 0H24v11.372H12.623zM0 12.623h11.377V24H0zM12.623 12.623H24V24H12.623z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Reddit -->
|
||||||
|
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 24 24"
|
||||||
|
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Amazon -->
|
||||||
|
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 48 48"
|
||||||
|
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M25.4026553,25.9595294 C24.660417,27.4418824 23.3876054,28.3962353 22.0103725,28.7181176 C21.8015298,28.7181176 21.4826213,28.8225882 21.1637129,28.8225882 C18.835399,28.8225882 17.458166,27.0211765 17.458166,24.3727059 C17.458166,20.9788235 19.4703937,19.392 22.0103725,18.6465882 C23.3876054,18.3303529 24.9793255,18.2230588 26.5682233,18.2230588 L26.5682233,19.4964706 C26.5682233,21.9331765 26.6726447,23.8390588 25.4026553,25.9595294 L25.4026553,25.9595294 Z M26.5682233,13.3524706 C25.1909904,13.4569412 23.5992703,13.5614118 22.0103725,13.7703529 C19.574815,14.0922353 17.1392576,14.5157647 15.1298521,15.4701176 C11.2098182,17.0597647 8.55977364,20.4508235 8.55977364,25.4287059 C8.55977364,31.6856471 12.5842289,34.8621176 17.6726531,34.8621176 C19.3659723,34.8621176 20.7432053,34.6475294 22.0103725,34.3341176 C24.0282445,33.696 25.7187415,32.5298824 27.7309692,30.4094118 C28.8965372,31.9990588 29.2182679,32.7444706 31.2276733,34.4385882 C31.7582467,34.6475294 32.28882,34.6475294 32.7093276,34.3341176 C33.9821392,33.2724706 36.208854,31.3637647 37.3715998,30.3049412 C37.9021732,29.8814118 37.7977518,29.2432941 37.4760212,28.7181176 C36.3132753,27.2329412 35.1448851,25.9595294 35.1448851,23.0992941 L35.1448851,13.5614118 C35.1448851,9.53505882 35.4666157,5.82494118 32.5004849,3.072 C30.0649275,0.849882353 26.2493149,0 23.2831841,0 L22.0103725,0 C16.6115064,0.313411765 10.8937319,2.64564706 9.61809814,9.32329412 C9.40643324,10.1731765 10.0442501,10.4894118 10.4675799,10.5938824 L16.3998415,11.3364706 C17.0348362,11.2291765 17.3537447,10.6983529 17.458166,10.1731765 C17.9859172,7.84094118 19.8937235,6.67482353 22.0103725,6.46023529 L22.4365245,6.46023529 C23.7093361,6.46023529 25.086569,6.99105882 25.8259851,8.05270588 C26.6726447,9.32329412 26.5682233,11.0202353 26.5682233,12.5054118 L26.5682233,13.3524706 L26.5682233,13.3524706 Z">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
d="M47.9943556,35.9463529 L47.9943556,35.9435294 C47.971778,35.4437647 47.8673567,35.0625882 47.658514,34.7463529 L47.6359364,34.7152941 L47.6105366,34.6842353 C47.3988717,34.4527059 47.1956734,34.3651765 46.9755419,34.2691765 C46.3179696,34.0150588 45.3612442,33.8795294 44.2097872,33.8767059 C43.382883,33.8767059 42.4713128,33.9557647 41.5540982,34.1562353 L41.551276,34.0941176 L40.6284171,34.4018824 L40.6114839,34.4103529 L40.0893771,34.5797647 L40.0893771,34.6023529 C39.47696,34.8564706 38.9209869,35.1727059 38.4045245,35.5482353 C38.0827939,35.7882353 37.8175072,36.1072941 37.8033962,36.5957647 C37.7949296,36.8611765 37.9303952,37.1661176 38.1533489,37.3468235 C38.3763025,37.5275294 38.6359448,37.5896471 38.8645429,37.5896471 C38.9181647,37.5896471 38.9689643,37.5868235 39.0141194,37.5783529 L39.0592746,37.5755294 L39.093141,37.5698824 C39.5446928,37.4738824 40.2022651,37.4089412 40.9727253,37.3016471 C41.6331198,37.2282353 42.3330251,37.1745882 42.9397978,37.1745882 C43.368772,37.1717647 43.7554132,37.2028235 44.0206999,37.2592941 C44.1533432,37.2875294 44.2521202,37.3214118 44.3057419,37.3496471 C44.3254973,37.3552941 44.3396083,37.3637647 44.3480749,37.3694118 C44.3593637,37.4061176 44.3762969,37.5021176 44.3734747,37.6348235 C44.3791191,38.1430588 44.164632,39.0861176 43.8683012,40.0065882 C43.5804369,40.9270588 43.2304843,41.8503529 42.999064,42.4630588 C42.94262,42.6042353 42.9059314,42.7595294 42.9059314,42.9289412 C42.900287,43.1745882 43.0018862,43.4738824 43.2163733,43.6715294 C43.425216,43.8691765 43.696147,43.9482353 43.9219229,43.9482353 L43.9332117,43.9482353 C44.2718756,43.9454118 44.5597398,43.8098824 44.8080933,43.6150588 C47.1505182,41.5087059 47.9661336,38.1430588 48,36.2484706 L47.9943556,35.9463529 Z M41.0489247,38.8658824 C40.8090378,38.8630588 40.5635065,38.9195294 40.3349084,39.0268235 C40.0780883,39.1284706 39.8156239,39.2470588 39.5672704,39.3515294 L39.2032068,39.504 L38.7290774,39.6931765 L38.7290774,39.6988235 C33.5785648,41.7882353 28.16841,43.0136471 23.1618295,43.1209412 C22.9783866,43.1265882 22.7921215,43.1265882 22.614323,43.1265882 C14.7403887,43.1322353 8.31706456,39.4785882 1.83729642,35.8785882 C1.61152053,35.76 1.37727804,35.6978824 1.15150215,35.6978824 C0.860815683,35.6978824 0.561662624,35.808 0.344353327,36.0112941 C0.12704403,36.2174118 -0.00277710907,36.5138824 4.50895989e-05,36.816 C-0.00277710907,37.2084706 0.208887791,37.5698824 0.505218651,37.8042353 C6.58705678,43.0870588 13.25309,47.9943529 22.2192152,48 C22.3941915,48 22.57199,47.9943529 22.7497885,47.9915294 C28.453452,47.8644706 34.902176,45.936 39.9087564,42.7905882 L39.9398006,42.7708235 C40.5945507,42.3783529 41.2493008,41.9322353 41.8673623,41.4381176 C42.2511813,41.1529412 42.516468,40.7068235 42.516468,40.2437647 C42.4995348,39.4221176 41.8024517,38.8658824 41.0489247,38.8658824 Z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- Oracle -->
|
||||||
|
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 16 16"
|
||||||
|
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd"
|
<path fill-rule="evenodd"
|
||||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
d="M.1 8c0 2.761 2.237 5 4.997 5h5.806A4.999 4.999 0 0015.9 8c0-2.761-2.237-5-4.997-5H5.097A4.999 4.999 0 00.1 8zm13.911 0a3.235 3.235 0 01-3.234 3.237h-5.55A3.235 3.235 0 011.991 8a3.235 3.235 0 013.234-3.236h5.551A3.235 3.235 0 0114.011 8z"
|
||||||
clip-rule="evenodd" />
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span id="github-stars">-</span>
|
<!-- Apple -->
|
||||||
</a>
|
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors -ml-3" viewBox="0 0 24 24"
|
||||||
|
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.78 1.18-.19 2.31-.89 3.51-.84 1.54.06 2.77.74 3.69 1.74-3.54 1.84-2.86 6.53.51 7.91-.48 1.13-1.1 2.25-2.79 3.38zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -192,7 +277,8 @@
|
|||||||
class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
|
class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
placeholder="Search for a tool (e.g., 'split', 'organize'...)" />
|
placeholder="Search for a tool (e.g., 'split', 'organize'...)" />
|
||||||
<span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2 gap-2">
|
<span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2 gap-2">
|
||||||
<kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd>
|
<kbd id="shortcut"
|
||||||
|
class="bg-gray-800 px-2.5 py-1.5 rounded-md text-xs font-semibold text-gray-300 border border-gray-700 shadow-sm"></kbd>
|
||||||
<button id="open-shortcuts-btn"
|
<button id="open-shortcuts-btn"
|
||||||
class="p-1 hover:bg-gray-600 rounded-md text-gray-400 hover:text-white transition-colors hidden-on-touch"
|
class="p-1 hover:bg-gray-600 rounded-md text-gray-400 hover:text-white transition-colors hidden-on-touch"
|
||||||
title="Keyboard Shortcuts">
|
title="Keyboard Shortcuts">
|
||||||
|
|||||||
+13
-3
@@ -13,7 +13,10 @@
|
|||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div 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" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<span class="text-white font-bold text-xl ml-2">
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
<a href="index.html">BentoPDF</a>
|
<a href="index.html">BentoPDF</a>
|
||||||
</span>
|
</span>
|
||||||
@@ -192,7 +195,8 @@
|
|||||||
<div
|
<div
|
||||||
class="mt-8 bg-gradient-to-r from-indigo-900/40 to-purple-900/40 rounded-lg p-6 border border-indigo-500/50">
|
class="mt-8 bg-gradient-to-r from-indigo-900/40 to-purple-900/40 rounded-lg p-6 border border-indigo-500/50">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="inline-block bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full mb-3 animate-pulse">
|
<div
|
||||||
|
class="inline-block bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full mb-3 animate-pulse">
|
||||||
LIMITED TIME OFFER
|
LIMITED TIME OFFER
|
||||||
</div>
|
</div>
|
||||||
<p class="text-white text-2xl font-bold mb-2">Lifetime License</p>
|
<p class="text-white text-2xl font-bold mb-2">Lifetime License</p>
|
||||||
@@ -210,7 +214,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<h4 class="text-white font-semibold text-sm mb-2">Unlimited Usage</h4>
|
<h4 class="text-white font-semibold text-sm mb-2">Unlimited Usage</h4>
|
||||||
<p class="text-gray-300 text-xs leading-relaxed">
|
<p class="text-gray-300 text-xs leading-relaxed">
|
||||||
The BentoPDF commercial license permits use on an <strong class="text-white">unrestricted, unlimited number of devices, servers, and user machines</strong> within your organization. There are <strong class="text-white">no per-user, per-machine, or per-seat limitations</strong>. Once you purchase the license, all your internal users may use BentoPDF through your browser-based system without requiring additional licenses.
|
The BentoPDF commercial license permits use on an <strong
|
||||||
|
class="text-white">unrestricted, unlimited number of devices, servers,
|
||||||
|
and user machines</strong> within your organization. There are <strong
|
||||||
|
class="text-white">no per-user, per-machine, or per-seat
|
||||||
|
limitations</strong>. Once you purchase the license, all your internal
|
||||||
|
users may use BentoPDF through your browser-based system without requiring
|
||||||
|
additional licenses.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ http {
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
listen 8080;
|
listen 8080;
|
||||||
|
listen [::]:8080;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.0",
|
"version": "1.10.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+4
-1
@@ -13,7 +13,10 @@
|
|||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div 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" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<span class="text-white font-bold text-xl ml-2">
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
<a href="index.html">BentoPDF</a>
|
<a href="index.html">BentoPDF</a>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 4.4 KiB |
@@ -1,5 +1,7 @@
|
|||||||
<svg width="86" height="166" viewBox="0 0 86 166" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="166" height="166" viewBox="0 0 166 166" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g transform="translate(40, 0)">
|
||||||
<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="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="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"/>
|
<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"/>
|
||||||
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 510 B After Width: | Height: | Size: 550 B |
+23
@@ -0,0 +1,23 @@
|
|||||||
|
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
||||||
|
|
||||||
|
interface InterleaveFile {
|
||||||
|
name: string;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InterleaveMessage {
|
||||||
|
command: 'interleave';
|
||||||
|
files: InterleaveFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InterleaveSuccessResponse {
|
||||||
|
status: 'success';
|
||||||
|
pdfBytes: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InterleaveErrorResponse {
|
||||||
|
status: 'error';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterleaveResponse = InterleaveSuccessResponse | InterleaveErrorResponse;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
self.importScripts('/coherentpdf.browser.min.js');
|
||||||
|
|
||||||
|
self.onmessage = function (e) {
|
||||||
|
const { command, files } = e.data;
|
||||||
|
|
||||||
|
if (command === 'interleave') {
|
||||||
|
interleavePDFs(files);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function interleavePDFs(files) {
|
||||||
|
try {
|
||||||
|
const loadedPdfs = [];
|
||||||
|
const pageCounts = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const uint8Array = new Uint8Array(file.data);
|
||||||
|
const pdfDoc = coherentpdf.fromMemory(uint8Array, "");
|
||||||
|
loadedPdfs.push(pdfDoc);
|
||||||
|
pageCounts.push(coherentpdf.pages(pdfDoc));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadedPdfs.length < 2) {
|
||||||
|
throw new Error('At least two PDF files are required for interleaving.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxPages = Math.max(...pageCounts);
|
||||||
|
|
||||||
|
const pdfsToMerge = [];
|
||||||
|
const rangesToMerge = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= maxPages; i++) {
|
||||||
|
for (let j = 0; j < loadedPdfs.length; j++) {
|
||||||
|
if (i <= pageCounts[j]) {
|
||||||
|
pdfsToMerge.push(loadedPdfs[j]);
|
||||||
|
rangesToMerge.push(coherentpdf.range(i, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdfsToMerge.length === 0) {
|
||||||
|
throw new Error('No valid pages to merge.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedPdf = coherentpdf.mergeSame(pdfsToMerge, true, true, rangesToMerge);
|
||||||
|
|
||||||
|
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
||||||
|
const buffer = mergedPdfBytes.buffer;
|
||||||
|
coherentpdf.deletePdf(mergedPdf);
|
||||||
|
loadedPdfs.forEach(pdf => coherentpdf.deletePdf(pdf));
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
status: 'success',
|
||||||
|
pdfBytes: buffer
|
||||||
|
}, [buffer]);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message || 'Unknown error during interleave merge'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+33
@@ -0,0 +1,33 @@
|
|||||||
|
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
||||||
|
|
||||||
|
interface MergeJob {
|
||||||
|
fileName: string;
|
||||||
|
rangeType: 'all' | 'specific' | 'single' | 'range';
|
||||||
|
rangeString?: string;
|
||||||
|
pageIndex?: number;
|
||||||
|
startPage?: number;
|
||||||
|
endPage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MergeFile {
|
||||||
|
name: string;
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MergeMessage {
|
||||||
|
command: 'merge';
|
||||||
|
files: MergeFile[];
|
||||||
|
jobs: MergeJob[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MergeSuccessResponse {
|
||||||
|
status: 'success';
|
||||||
|
pdfBytes: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MergeErrorResponse {
|
||||||
|
status: 'error';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MergeResponse = MergeSuccessResponse | MergeErrorResponse;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
self.importScripts('/coherentpdf.browser.min.js');
|
||||||
|
|
||||||
|
self.onmessage = function (e) {
|
||||||
|
const { command, files, jobs } = e.data;
|
||||||
|
|
||||||
|
if (command === 'merge') {
|
||||||
|
mergePDFs(files, jobs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function mergePDFs(files, jobs) {
|
||||||
|
try {
|
||||||
|
const loadedPdfs = {};
|
||||||
|
const pdfsToMerge = [];
|
||||||
|
const rangesToMerge = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const uint8Array = new Uint8Array(file.data);
|
||||||
|
const pdfDoc = coherentpdf.fromMemory(uint8Array, "");
|
||||||
|
loadedPdfs[file.name] = pdfDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
const sourcePdf = loadedPdfs[job.fileName];
|
||||||
|
if (!sourcePdf) continue;
|
||||||
|
|
||||||
|
let range;
|
||||||
|
if (job.rangeType === 'all') {
|
||||||
|
range = coherentpdf.all(sourcePdf);
|
||||||
|
} else if (job.rangeType === 'specific') {
|
||||||
|
if (coherentpdf.validatePagespec(job.rangeString)) {
|
||||||
|
range = coherentpdf.parsePagespec(sourcePdf, job.rangeString);
|
||||||
|
} else {
|
||||||
|
range = coherentpdf.all(sourcePdf);
|
||||||
|
}
|
||||||
|
} else if (job.rangeType === 'single') {
|
||||||
|
const pageNum = job.pageIndex + 1;
|
||||||
|
range = coherentpdf.range(pageNum, pageNum);
|
||||||
|
} else if (job.rangeType === 'range') {
|
||||||
|
range = coherentpdf.range(job.startPage, job.endPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfsToMerge.push(sourcePdf);
|
||||||
|
rangesToMerge.push(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdfsToMerge.length === 0) {
|
||||||
|
throw new Error('No valid files or pages to merge.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedPdf = coherentpdf.mergeSame(pdfsToMerge, true, true, rangesToMerge);
|
||||||
|
|
||||||
|
const mergedPdfBytes = coherentpdf.toMemory(mergedPdf, false, true);
|
||||||
|
const buffer = mergedPdfBytes.buffer;
|
||||||
|
|
||||||
|
coherentpdf.deletePdf(mergedPdf);
|
||||||
|
Object.values(loadedPdfs).forEach(pdf => coherentpdf.deletePdf(pdf));
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
status: 'success',
|
||||||
|
pdfBytes: buffer
|
||||||
|
}, [buffer]);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message: error.message || 'Unknown error during merge'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
-4
@@ -257,9 +257,11 @@ footer a {
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.section-divider {
|
.section-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(to right, transparent, #4f46e5, transparent);
|
background: linear-gradient(to right, transparent, #4d44f7, transparent);
|
||||||
/* Fades from transparent to indigo and back */
|
/* Fades from transparent to indigo and back */
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
/* my-16 */
|
/* my-16 */
|
||||||
@@ -289,9 +291,6 @@ footer a {
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
0 10px 15px -3px rgba(79, 70, 229, 0.3),
|
0 10px 15px -3px rgba(79, 70, 229, 0.3),
|
||||||
0 4px 6px -4px 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 {
|
.btn-gradient:focus {
|
||||||
@@ -589,3 +588,28 @@ button:disabled,
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-grid-pattern {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
background-size: 60px 60px;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
|
||||||
|
mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
|
||||||
|
-webkit-mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide spin buttons for number inputs */
|
||||||
|
input[type="number"] {
|
||||||
|
appearance: none;
|
||||||
|
-moz-appearance: textfield; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none; /* Chrome, Safari, Edge */
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ export const singlePdfLoadTools = [
|
|||||||
'pdf-to-greyscale',
|
'pdf-to-greyscale',
|
||||||
'edit-metadata',
|
'edit-metadata',
|
||||||
'remove-metadata',
|
'remove-metadata',
|
||||||
'flatten',
|
|
||||||
'delete-pages',
|
'delete-pages',
|
||||||
'add-blank-page',
|
'add-blank-page',
|
||||||
'extract-pages',
|
'extract-pages',
|
||||||
@@ -68,4 +68,6 @@ export const multiFileTools = [
|
|||||||
'txt-to-pdf',
|
'txt-to-pdf',
|
||||||
'compress',
|
'compress',
|
||||||
'extract-attachments',
|
'extract-attachments',
|
||||||
|
'flatten',
|
||||||
|
'repair-pdf',
|
||||||
];
|
];
|
||||||
@@ -13,7 +13,7 @@ export const categories = [
|
|||||||
id: 'merge',
|
id: 'merge',
|
||||||
name: 'Merge PDF',
|
name: 'Merge PDF',
|
||||||
icon: 'combine',
|
icon: 'combine',
|
||||||
subtitle: 'Combine multiple PDFs into one file.',
|
subtitle: 'Combine multiple PDFs into one file. Preserves Bookmarks.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'split',
|
id: 'split',
|
||||||
@@ -310,7 +310,7 @@ export const categories = [
|
|||||||
id: 'alternate-merge',
|
id: 'alternate-merge',
|
||||||
name: 'Alternate & Mix Pages',
|
name: 'Alternate & Mix Pages',
|
||||||
icon: 'shuffle',
|
icon: 'shuffle',
|
||||||
subtitle: 'Combine PDFs by alternating pages from each.',
|
subtitle: 'Merge PDFs by alternating pages from each PDF. Preserves Bookmarks',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'organize',
|
id: 'organize',
|
||||||
@@ -468,6 +468,12 @@ export const categories = [
|
|||||||
subtitle:
|
subtitle:
|
||||||
'Remove password protection and security restrictions associated with digitally signed PDF files.',
|
'Remove password protection and security restrictions associated with digitally signed PDF files.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/src/pages/repair-pdf.html',
|
||||||
|
name: 'Repair PDF',
|
||||||
|
icon: 'wrench',
|
||||||
|
subtitle: 'Recover data from corrupted or damaged PDF files.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'organize' || toolId === 'rotate') {
|
if (toolId === 'organize' || toolId === 'rotate' || toolId === 'delete-pages') {
|
||||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||||
|
|
||||||
if (toolId === 'rotate') {
|
if (toolId === 'rotate') {
|
||||||
@@ -134,14 +134,19 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const rotateAllRightBtn = document.getElementById(
|
const rotateAllRightBtn = document.getElementById(
|
||||||
'rotate-all-right-btn'
|
'rotate-all-right-btn'
|
||||||
);
|
);
|
||||||
|
const rotateAllCustomBtn = document.getElementById('rotate-all-custom-btn');
|
||||||
|
const rotateAllCustomInput = document.getElementById('custom-rotate-all-input') as HTMLInputElement;
|
||||||
|
const rotateAllDecrementBtn = document.getElementById('rotate-all-decrement-btn');
|
||||||
|
const rotateAllIncrementBtn = document.getElementById('rotate-all-increment-btn');
|
||||||
|
|
||||||
|
|
||||||
rotateAllControls.classList.remove('hidden');
|
rotateAllControls.classList.remove('hidden');
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const rotateAll = (direction) => {
|
const rotateAll = (angle: number) => {
|
||||||
// Update rotation state for ALL pages (including unrendered ones)
|
// Update rotation state for ALL pages (including unrendered ones)
|
||||||
for (let i = 0; i < rotationState.length; i++) {
|
for (let i = 0; i < rotationState.length; i++) {
|
||||||
rotationState[i] = (rotationState[i] + direction * 90 + 360) % 360;
|
rotationState[i] = (rotationState[i] + angle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update DOM for currently rendered pages
|
// Update DOM for currently rendered pages
|
||||||
@@ -149,15 +154,44 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0');
|
const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0');
|
||||||
const newRotation = rotationState[pageIndex];
|
const newRotation = rotationState[pageIndex];
|
||||||
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||||
|
|
||||||
const thumbnail = item.querySelector('canvas, img');
|
const thumbnail = item.querySelector('canvas, img');
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
(thumbnail as HTMLElement).style.transform =
|
(thumbnail as HTMLElement).style.transform =
|
||||||
`rotate(${newRotation}deg)`;
|
`rotate(${newRotation}deg)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const input = item.querySelector('input');
|
||||||
|
if (input) {
|
||||||
|
input.value = newRotation.toString();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
rotateAllLeftBtn.onclick = () => rotateAll(-1);
|
rotateAllLeftBtn.onclick = () => rotateAll(-90);
|
||||||
rotateAllRightBtn.onclick = () => rotateAll(1);
|
rotateAllRightBtn.onclick = () => rotateAll(90);
|
||||||
|
|
||||||
|
if (rotateAllCustomBtn && rotateAllCustomInput) {
|
||||||
|
rotateAllCustomBtn.onclick = () => {
|
||||||
|
const angle = parseInt(rotateAllCustomInput.value);
|
||||||
|
if (!isNaN(angle) && angle !== 0) {
|
||||||
|
rotateAll(angle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rotateAllDecrementBtn) {
|
||||||
|
rotateAllDecrementBtn.onclick = () => {
|
||||||
|
let current = parseInt(rotateAllCustomInput.value) || 0;
|
||||||
|
rotateAllCustomInput.value = (current - 1).toString();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotateAllIncrementBtn) {
|
||||||
|
rotateAllIncrementBtn.onclick = () => {
|
||||||
|
let current = parseInt(rotateAllCustomInput.value) || 0;
|
||||||
|
rotateAllCustomInput.value = (current + 1).toString();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -754,7 +788,7 @@ export function setupFileInputHandler(toolId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMultiFileTool) {
|
if (isMultiFileTool) {
|
||||||
if (toolId === 'txt-to-pdf' || toolId === 'compress' || toolId === 'extract-attachments') {
|
if (toolId === 'txt-to-pdf' || toolId === 'compress' || toolId === 'extract-attachments' || toolId === 'flatten') {
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { PDFDocument } from 'pdf-lib';
|
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
const alternateMergeState = {
|
interface AlternateMergeState {
|
||||||
|
pdfDocs: Record<string, any>;
|
||||||
|
pdfBytes: Record<string, ArrayBuffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alternateMergeState: AlternateMergeState = {
|
||||||
pdfDocs: {},
|
pdfDocs: {},
|
||||||
|
pdfBytes: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const alternateMergeWorker = new Worker('/workers/alternate-merge.worker.js');
|
||||||
|
|
||||||
export async function setupAlternateMergeTool() {
|
export async function setupAlternateMergeTool() {
|
||||||
const optionsDiv = document.getElementById('alternate-merge-options');
|
const optionsDiv = document.getElementById('alternate-merge-options');
|
||||||
const processBtn = document.getElementById(
|
const processBtn = document.getElementById(
|
||||||
@@ -23,18 +30,18 @@ export async function setupAlternateMergeTool() {
|
|||||||
|
|
||||||
fileList.innerHTML = '';
|
fileList.innerHTML = '';
|
||||||
alternateMergeState.pdfDocs = {};
|
alternateMergeState.pdfDocs = {};
|
||||||
|
alternateMergeState.pdfBytes = {};
|
||||||
|
|
||||||
showLoader('Loading PDF documents...');
|
showLoader('Loading PDF documents...');
|
||||||
try {
|
try {
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(
|
alternateMergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
|
||||||
pdfBytes as ArrayBuffer,
|
|
||||||
{
|
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
|
||||||
ignoreEncryption: true,
|
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
||||||
}
|
alternateMergeState.pdfDocs[file.name] = pdfjsDoc;
|
||||||
);
|
const pageCount = pdfjsDoc.numPages;
|
||||||
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
|
|
||||||
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className =
|
li.className =
|
||||||
@@ -79,7 +86,7 @@ export async function setupAlternateMergeTool() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function alternateMerge() {
|
export async function alternateMerge() {
|
||||||
if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
|
if (Object.keys(alternateMergeState.pdfBytes).length < 2) {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Not Enough Files',
|
'Not Enough Files',
|
||||||
'Please upload at least two PDF files to alternate and mix.'
|
'Please upload at least two PDF files to alternate and mix.'
|
||||||
@@ -89,37 +96,55 @@ export async function alternateMerge() {
|
|||||||
|
|
||||||
showLoader('Alternating and mixing pages...');
|
showLoader('Alternating and mixing pages...');
|
||||||
try {
|
try {
|
||||||
const newPdfDoc = await PDFDocument.create();
|
|
||||||
const fileList = document.getElementById('alternate-file-list');
|
const fileList = document.getElementById('alternate-file-list');
|
||||||
|
if (!fileList) throw new Error('File list not found');
|
||||||
|
|
||||||
const sortedFileNames = Array.from(fileList.children).map(
|
const sortedFileNames = Array.from(fileList.children).map(
|
||||||
(li) => (li as HTMLElement).dataset.fileName
|
(li) => (li as HTMLElement).dataset.fileName
|
||||||
);
|
).filter(Boolean) as string[];
|
||||||
|
|
||||||
const loadedDocs = sortedFileNames.map(
|
const filesToMerge: InterleaveFile[] = [];
|
||||||
(name) => alternateMergeState.pdfDocs[name]
|
for (const name of sortedFileNames) {
|
||||||
);
|
const bytes = alternateMergeState.pdfBytes[name];
|
||||||
const pageCounts = loadedDocs.map((doc) => doc.getPageCount());
|
if (bytes) {
|
||||||
const maxPages = Math.max(...pageCounts);
|
filesToMerge.push({ name, data: bytes });
|
||||||
|
|
||||||
for (let i = 0; i < maxPages; i++) {
|
|
||||||
for (const doc of loadedDocs) {
|
|
||||||
if (i < doc.getPageCount()) {
|
|
||||||
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
|
|
||||||
newPdfDoc.addPage(copiedPage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedPdfBytes = await newPdfDoc.save();
|
if (filesToMerge.length < 2) {
|
||||||
downloadFile(
|
showAlert('Error', 'At least two valid PDFs are required.');
|
||||||
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
hideLoader();
|
||||||
'alternated-mixed.pdf'
|
return;
|
||||||
);
|
}
|
||||||
showAlert('Success', 'PDFs have been mixed successfully!');
|
|
||||||
|
const message: InterleaveMessage = {
|
||||||
|
command: 'interleave',
|
||||||
|
files: filesToMerge
|
||||||
|
};
|
||||||
|
|
||||||
|
alternateMergeWorker.postMessage(message, filesToMerge.map(f => f.data));
|
||||||
|
|
||||||
|
alternateMergeWorker.onmessage = (e: MessageEvent<InterleaveResponse>) => {
|
||||||
|
hideLoader();
|
||||||
|
if (e.data.status === 'success') {
|
||||||
|
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
||||||
|
downloadFile(blob, 'alternated-mixed.pdf');
|
||||||
|
showAlert('Success', 'PDFs have been mixed successfully!');
|
||||||
|
} else {
|
||||||
|
console.error('Worker interleave error:', e.data.message);
|
||||||
|
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
alternateMergeWorker.onerror = (e) => {
|
||||||
|
hideLoader();
|
||||||
|
console.error('Worker error:', e);
|
||||||
|
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
||||||
|
};
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Alternate Merge error:', e);
|
console.error('Alternate Merge error:', e);
|
||||||
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||||
} finally {
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+188
-322
@@ -1,7 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
// TODO: @ALAM - remove ts-nocheck and fix types later, possibly convert this into an npm package
|
// TODO: @ALAM - remove ts-nocheck and fix types later, possibly convert this into an npm package
|
||||||
|
|
||||||
import { PDFDocument, PDFName, PDFString, PDFNumber, PDFArray } from 'pdf-lib';
|
import { PDFDocument, PDFName, PDFString, PDFNumber, PDFArray, PDFHexString } from 'pdf-lib';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
@@ -37,19 +37,19 @@ function showInputModal(title, fields = [], defaultValues = {}) {
|
|||||||
.map((field) => {
|
.map((field) => {
|
||||||
if (field.type === 'text') {
|
if (field.type === 'text') {
|
||||||
return `
|
return `
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
|
||||||
<input type="text" id="modal-${field.name}" value="${escapeHTML(defaultValues[field.name] || '')}"
|
<input type="text" id="modal-${field.name}" value="${escapeHTML(defaultValues[field.name] || '')}"
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||||
placeholder="${field.placeholder || ''}" />
|
placeholder="${field.placeholder || ''}" />
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (field.type === 'select') {
|
} else if (field.type === 'select') {
|
||||||
return `
|
return `
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
|
||||||
<select id="modal-${field.name}" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
|
<select id="modal-${field.name}" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
|
||||||
${field.options
|
${field.options
|
||||||
.map(
|
.map(
|
||||||
(opt) => `
|
(opt) => `
|
||||||
<option value="${opt.value}" ${defaultValues[field.name] === opt.value ? 'selected' : ''}>
|
<option value="${opt.value}" ${defaultValues[field.name] === opt.value ? 'selected' : ''}>
|
||||||
@@ -57,91 +57,92 @@ function showInputModal(title, fields = [], defaultValues = {}) {
|
|||||||
</option>
|
</option>
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.join('')}
|
.join('')
|
||||||
</select>
|
}
|
||||||
|
</select>
|
||||||
${field.name === 'color' ? '<input type="color" id="modal-color-picker" class="hidden w-full h-10 mt-2 rounded cursor-pointer border border-gray-300" value="#000000" />' : ''}
|
${field.name === 'color' ? '<input type="color" id="modal-color-picker" class="hidden w-full h-10 mt-2 rounded cursor-pointer border border-gray-300" value="#000000" />' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (field.type === 'destination') {
|
} else if (field.type === 'destination') {
|
||||||
const hasDestination =
|
const hasDestination =
|
||||||
defaultValues.destX !== null && defaultValues.destX !== undefined;
|
defaultValues.destX !== null && defaultValues.destX !== undefined;
|
||||||
return `
|
return `
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
|
||||||
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200 space-y-2">
|
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200 space-y-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label class="flex items-center gap-1 text-xs">
|
<label class="flex items-center gap-1 text-xs">
|
||||||
<input type="checkbox" id="modal-use-destination" class="w-4 h-4" ${hasDestination ? 'checked' : ''}>
|
<input type="checkbox" id="modal-use-destination" class="w-4 h-4" ${hasDestination ? 'checked' : ''}>
|
||||||
<span>Set custom destination</span>
|
<span>Set custom destination </span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="destination-controls" class="${hasDestination ? '' : 'hidden'} space-y-2">
|
<div id="destination-controls" class="${hasDestination ? '' : 'hidden'} space-y-2">
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-600">Page</label>
|
<label class="text-xs text-gray-600">Page</label>
|
||||||
<input type="number" id="modal-dest-page" min="1" max="${field.maxPages || 1}" value="${defaultValues.destPage || field.page || 1}"
|
<input type="number" id="modal-dest-page" min="1" max="${field.maxPages || 1}" value="${defaultValues.destPage || field.page || 1}"
|
||||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" step="1" />
|
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" step="1" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-600">Zoom (%)</label>
|
<label class="text-xs text-gray-600">Zoom(%)</label>
|
||||||
<select id="modal-dest-zoom" class="w-full px-2 py-1 border border-gray-300 rounded text-sm">
|
<select id="modal-dest-zoom" class="w-full px-2 py-1 border border-gray-300 rounded text-sm">
|
||||||
<option value="">Inherit</option>
|
<option value="">Inherit</option>
|
||||||
<option value="0">Fit Page</option>
|
<option value="0">Fit Page</option>
|
||||||
<option value="50">50%</option>
|
<option value="50">50%</option>
|
||||||
<option value="75">75%</option>
|
<option value="75">75%</option>
|
||||||
<option value="100">100%</option>
|
<option value="100">100%</option>
|
||||||
<option value="125">125%</option>
|
<option value="125">125%</option>
|
||||||
<option value="150">150%</option>
|
<option value="150">150%</option>
|
||||||
<option value="200">200%</option>
|
<option value="200">200%</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<div class="grid grid-cols-2 gap-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-600">X Position</label>
|
<label class="text-xs text-gray-600">X Position</label>
|
||||||
<input type="number" id="modal-dest-x" value="0" step="10"
|
<input type="number" id="modal-dest-x" value="0" step="10"
|
||||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
|
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-xs text-gray-600">Y Position</label>
|
<label class="text-xs text-gray-600">Y Position</label>
|
||||||
<input type="number" id="modal-dest-y" value="0" step="10"
|
<input type="number" id="modal-dest-y" value="0" step="10"
|
||||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
|
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button id="modal-pick-destination" class="w-full px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-xs flex items-center justify-center gap-1">
|
<button id="modal-pick-destination" class="w-full px-3 py-2 btn-gradient text-white rounded text-xs !flex items-center justify-center gap-1">
|
||||||
<i data-lucide="crosshair" class="w-3 h-3"></i> Click on PDF to Pick Location
|
<i data-lucide="crosshair" class="w-3 h-3"></i> Click on PDF to Pick Location
|
||||||
</button>
|
</button>
|
||||||
<p class="text-xs text-gray-500 italic">Click the button above, then click on the PDF where you want the bookmark to jump to</p>
|
<p class="text-xs text-gray-500 italic">Click the button above, then click on the PDF where you want the bookmark to jump to</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (field.type === 'preview') {
|
} else if (field.type === 'preview') {
|
||||||
return `
|
return `
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
|
||||||
<div id="modal-preview" class="style-preview bg-gray-50">
|
<div id="modal-preview" class="style-preview bg-gray-50">
|
||||||
<span id="preview-text" style="font-size: 16px;">Preview Text</span>
|
<span id="preview-text" style="font-size: 16px;">Preview Text</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
})
|
})
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
|
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
${fieldsHTML}
|
${fieldsHTML}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2 justify-end">
|
<div class="flex gap-2 justify-end">
|
||||||
<button id="modal-cancel" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300">Cancel</button>
|
<button id="modal-cancel" class="px-4 py-2 rounded-lg bg-gray-200 hover:bg-gray-300">Cancel</button>
|
||||||
<button id="modal-confirm" class="px-4 py-2 rounded bg-blue-500 hover:bg-blue-600 text-white">Confirm</button>
|
<button id="modal-confirm" class="px-4 py-2 rounded btn-gradient text-white">Confirm</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
overlay.appendChild(modal);
|
overlay.appendChild(modal);
|
||||||
modalContainer.appendChild(overlay);
|
modalContainer.appendChild(overlay);
|
||||||
@@ -476,7 +477,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
canvasWrapper.appendChild(coordTooltip);
|
canvasWrapper.appendChild(coordTooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
coordTooltip.textContent = `X: ${Math.round(x)}, Y: ${Math.round(y)}`;
|
coordTooltip.textContent = `X: ${Math.round(x)}, Y: ${Math.round(y)} `;
|
||||||
coordTooltip.style.left = e.clientX - rect.left + 15 + 'px';
|
coordTooltip.style.left = e.clientX - rect.left + 15 + 'px';
|
||||||
coordTooltip.style.top = e.clientY - rect.top + 15 + 'px';
|
coordTooltip.style.top = e.clientY - rect.top + 15 + 'px';
|
||||||
});
|
});
|
||||||
@@ -525,12 +526,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
destinationMarker = document.createElement('div');
|
destinationMarker = document.createElement('div');
|
||||||
destinationMarker.className = 'destination-marker';
|
destinationMarker.className = 'destination-marker';
|
||||||
destinationMarker.innerHTML = `
|
destinationMarker.innerHTML = `
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="10" fill="#3b82f6" fill-opacity="0.2"/>
|
<circle cx="12" cy="12" r="10" fill="#3b82f6" fill-opacity="0.2" />
|
||||||
<path d="M12 2 L12 22 M2 12 L22 12"/>
|
<path d="M12 2 L12 22 M2 12 L22 12" />
|
||||||
<circle cx="12" cy="12" r="2" fill="#3b82f6"/>
|
<circle cx="12" cy="12" r="2" fill="#3b82f6" />
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
const canvasRect = canvas.getBoundingClientRect();
|
const canvasRect = canvas.getBoundingClientRect();
|
||||||
const wrapperRect = canvasWrapper.getBoundingClientRect();
|
const wrapperRect = canvasWrapper.getBoundingClientRect();
|
||||||
destinationMarker.style.position = 'absolute';
|
destinationMarker.style.position = 'absolute';
|
||||||
@@ -549,7 +550,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
canvasX + canvasRect.left - wrapperRect.left + 20 + 'px';
|
canvasX + canvasRect.left - wrapperRect.left + 20 + 'px';
|
||||||
coordDisplay.style.top =
|
coordDisplay.style.top =
|
||||||
canvasY + canvasRect.top - wrapperRect.top - 30 + 'px';
|
canvasY + canvasRect.top - wrapperRect.top - 30 + 'px';
|
||||||
coordDisplay.textContent = `X: ${Math.round(pdfX)}, Y: ${Math.round(pdfY)}`;
|
coordDisplay.textContent = `X: ${Math.round(pdfX)}, Y: ${Math.round(pdfY)} `;
|
||||||
canvasWrapper.appendChild(coordDisplay);
|
canvasWrapper.appendChild(coordDisplay);
|
||||||
|
|
||||||
// Call callback with PDF coordinates
|
// Call callback with PDF coordinates
|
||||||
@@ -577,14 +578,14 @@ function showConfirmModal(message) {
|
|||||||
modal.className = 'modal-content';
|
modal.className = 'modal-content';
|
||||||
|
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h3 class="text-xl font-bold text-gray-800 mb-4">Confirm Action</h3>
|
<h3 class="text-xl font-bold text-gray-800 mb-4">Confirm Action</h3>
|
||||||
<p class="text-gray-600 mb-6">${message}</p>
|
<p class="text-gray-600 mb-6">${message}</p>
|
||||||
<div class="flex gap-2 justify-end">
|
<div class="flex gap-2 justify-end">
|
||||||
<button id="modal-cancel" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300">Cancel</button>
|
<button id="modal-cancel" class="px-4 py-2 rounded-lg bg-gray-200 hover:bg-gray-300">Cancel</button>
|
||||||
<button id="modal-confirm" class="px-4 py-2 rounded bg-blue-500 hover:bg-blue-600 text-white">Confirm</button>
|
<button id="modal-confirm" class="px-4 py-2 rounded btn-gradient text-white">Confirm</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
overlay.appendChild(modal);
|
overlay.appendChild(modal);
|
||||||
@@ -618,14 +619,14 @@ function showAlertModal(title, message) {
|
|||||||
modal.className = 'modal-content';
|
modal.className = 'modal-content';
|
||||||
|
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
|
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
|
||||||
<p class="text-gray-600 mb-6">${message}</p>
|
<p class="text-gray-600 mb-6">${message}</p>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<button id="modal-ok" class="px-4 py-2 rounded bg-blue-500 hover:bg-blue-600 text-white">OK</button>
|
<button id="modal-ok" class="px-4 py-2 rounded btn-gradient text-white">OK</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
overlay.appendChild(modal);
|
overlay.appendChild(modal);
|
||||||
modalContainer.appendChild(overlay);
|
modalContainer.appendChild(overlay);
|
||||||
@@ -832,7 +833,7 @@ deleteAllBtn.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = await showConfirmModal(
|
const confirmed = await showConfirmModal(
|
||||||
`Delete all ${bookmarkTree.length} bookmark(s)?`
|
`Delete all ${bookmarkTree.length} bookmark(s) ? `
|
||||||
);
|
);
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
bookmarkTree = [];
|
bookmarkTree = [];
|
||||||
@@ -949,7 +950,7 @@ batchDeleteBtn.addEventListener('click', async () => {
|
|||||||
if (selectedBookmarks.size === 0) return;
|
if (selectedBookmarks.size === 0) return;
|
||||||
|
|
||||||
const confirmed = await showConfirmModal(
|
const confirmed = await showConfirmModal(
|
||||||
`Delete ${selectedBookmarks.size} bookmark(s)?`
|
`Delete ${selectedBookmarks.size} bookmark(s) ? `
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
@@ -1173,7 +1174,7 @@ async function renderPage(num, zoom = null, destX = null, destY = null) {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Draw coordinate text background
|
// Draw coordinate text background
|
||||||
const text = `X: ${Math.round(destX)}, Y: ${Math.round(destY)}`;
|
const text = `X: ${Math.round(destX)}, Y: ${Math.round(destY)} `;
|
||||||
ctx.font = 'bold 12px monospace';
|
ctx.font = 'bold 12px monospace';
|
||||||
const textMetrics = ctx.measureText(text);
|
const textMetrics = ctx.measureText(text);
|
||||||
const textWidth = textMetrics.width;
|
const textWidth = textMetrics.width;
|
||||||
@@ -1532,7 +1533,7 @@ function createNodeElement(node, level = 0) {
|
|||||||
if (result && result.title) {
|
if (result && result.title) {
|
||||||
node.children.push({
|
node.children.push({
|
||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
title: result.title,
|
title: cleanTitle(result.title),
|
||||||
page: currentPage,
|
page: currentPage,
|
||||||
children: [],
|
children: [],
|
||||||
color: null,
|
color: null,
|
||||||
@@ -1608,7 +1609,7 @@ function createNodeElement(node, level = 0) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
node.title = result.title;
|
node.title = cleanTitle(result.title);
|
||||||
node.color = result.color || null;
|
node.color = result.color || null;
|
||||||
node.style = result.style || null;
|
node.style = result.style || null;
|
||||||
|
|
||||||
@@ -1756,7 +1757,7 @@ function parseCSV(text) {
|
|||||||
const [, title, page, level] = match;
|
const [, title, page, level] = match;
|
||||||
const bookmark = {
|
const bookmark = {
|
||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
title: title.replace(/""/g, '"'),
|
title: cleanTitle(title.replace(/""/g, '"')),
|
||||||
page: parseInt(page),
|
page: parseInt(page),
|
||||||
children: [],
|
children: [],
|
||||||
color: null,
|
color: null,
|
||||||
@@ -1787,6 +1788,15 @@ jsonImportHidden.addEventListener('change', async (e) => {
|
|||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
try {
|
try {
|
||||||
const imported = JSON.parse(text);
|
const imported = JSON.parse(text);
|
||||||
|
// Recursively clean titles in imported JSON
|
||||||
|
function cleanImportedTree(nodes) {
|
||||||
|
if (!nodes) return;
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.title) node.title = cleanTitle(node.title);
|
||||||
|
if (node.children) cleanImportedTree(node.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleanImportedTree(imported);
|
||||||
bookmarkTree = imported;
|
bookmarkTree = imported;
|
||||||
saveState();
|
saveState();
|
||||||
renderBookmarkTree();
|
renderBookmarkTree();
|
||||||
@@ -1834,215 +1844,84 @@ extractExistingBtn.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// function cleanTitle(title) {
|
||||||
|
// // @TODO@ALAM: visit this for encoding issues later
|
||||||
|
// if (typeof title === 'string') {
|
||||||
|
// if (title.includes('€') && !title.includes(' ')) {
|
||||||
|
// return title.replace(/€/g, ' ');
|
||||||
|
// }
|
||||||
|
// return title.replace(/[\x00-\x1F\x7F-\x9F]/g, '').trim();
|
||||||
|
// }
|
||||||
|
// return title;
|
||||||
|
// }
|
||||||
|
|
||||||
|
function cleanTitle(title) {
|
||||||
|
// @TODO@ALAM: check for other encoding issues
|
||||||
|
if (typeof title === 'string') {
|
||||||
|
return title.replace(/[\x00-\x1F\x7F-\x9F]/g, '').trim();
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
async function extractExistingBookmarks(doc) {
|
async function extractExistingBookmarks(doc) {
|
||||||
try {
|
try {
|
||||||
const outlines = doc.catalog.lookup(PDFName.of('Outlines'));
|
const outline = await pdfJsDoc.getOutline();
|
||||||
if (!outlines) return [];
|
console.log(outline);
|
||||||
|
if (!outline) return [];
|
||||||
const pages = doc.getPages();
|
|
||||||
|
|
||||||
// Helper to resolve references
|
|
||||||
function resolveRef(obj) {
|
|
||||||
if (!obj) return null;
|
|
||||||
if (obj.lookup) return obj;
|
|
||||||
if (obj.objectNumber !== undefined && doc.context) {
|
|
||||||
return doc.context.lookup(obj);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build named destinations map (support full name-tree with Kids and Catalog-level Dests)
|
|
||||||
const namedDests = new Map();
|
|
||||||
try {
|
|
||||||
function addNamePair(nameObj, destObj) {
|
|
||||||
try {
|
|
||||||
const key = nameObj.decodeText
|
|
||||||
? nameObj.decodeText()
|
|
||||||
: String(nameObj);
|
|
||||||
namedDests.set(key, resolveRef(destObj));
|
|
||||||
} catch (_) {
|
|
||||||
// ignore malformed entry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function traverseNamesNode(node) {
|
|
||||||
if (!node) return;
|
|
||||||
node = resolveRef(node);
|
|
||||||
if (!node) return;
|
|
||||||
|
|
||||||
const namesArray = node.lookup
|
|
||||||
? node.lookup(PDFName.of('Names'))
|
|
||||||
: null;
|
|
||||||
if (namesArray && namesArray.array) {
|
|
||||||
for (let i = 0; i < namesArray.array.length; i += 2) {
|
|
||||||
const n = namesArray.array[i];
|
|
||||||
const d = namesArray.array[i + 1];
|
|
||||||
addNamePair(n, d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const kidsArray = node.lookup ? node.lookup(PDFName.of('Kids')) : null;
|
|
||||||
if (kidsArray && kidsArray.array) {
|
|
||||||
for (const kid of kidsArray.array) traverseNamesNode(kid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Names tree under Catalog/Names/Dests
|
|
||||||
const names = doc.catalog.lookup(PDFName.of('Names'));
|
|
||||||
if (names) {
|
|
||||||
const destsTree = names.lookup(PDFName.of('Dests'));
|
|
||||||
if (destsTree) traverseNamesNode(destsTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some PDFs store Dests directly under the Catalog's Dests as a dictionary
|
|
||||||
const catalogDests = doc.catalog.lookup(PDFName.of('Dests'));
|
|
||||||
if (catalogDests && catalogDests.dict) {
|
|
||||||
const entries = catalogDests.dict.entries();
|
|
||||||
for (const [key, value] of entries) {
|
|
||||||
// keys are PDFName; convert to string
|
|
||||||
const keyStr = key.decodeText ? key.decodeText() : key.toString();
|
|
||||||
namedDests.set(keyStr, resolveRef(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error building named destinations:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPageIndex(pageRef) {
|
|
||||||
if (!pageRef) return 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resolved = resolveRef(pageRef);
|
|
||||||
|
|
||||||
if (pageRef.numberValue !== undefined) {
|
|
||||||
const numericIndex = pageRef.numberValue | 0;
|
|
||||||
if (numericIndex >= 0 && numericIndex < pages.length)
|
|
||||||
return numericIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageRef.objectNumber !== undefined) {
|
|
||||||
const idxByObjNum = pages.findIndex(
|
|
||||||
(p) => p.ref.objectNumber === pageRef.objectNumber
|
|
||||||
);
|
|
||||||
if (idxByObjNum !== -1) return idxByObjNum;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageRef.toString) {
|
|
||||||
const target = pageRef.toString();
|
|
||||||
const idxByString = pages.findIndex(
|
|
||||||
(p) => p.ref.toString() === target
|
|
||||||
);
|
|
||||||
if (idxByString !== -1) return idxByString;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resolved && resolved.get) {
|
|
||||||
for (let i = 0; i < pages.length; i++) {
|
|
||||||
const pageDict = doc.context.lookup(pages[i].ref);
|
|
||||||
if (pageDict === resolved) return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error finding page:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: keep current behavior but log for diagnostics
|
|
||||||
console.warn('Falling back to page 0 for destination');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDestination(item) {
|
|
||||||
if (!item) return null;
|
|
||||||
|
|
||||||
// Try Dest entry first
|
|
||||||
let dest = item.lookup(PDFName.of('Dest'));
|
|
||||||
|
|
||||||
// If no Dest, try Action/D
|
|
||||||
if (!dest) {
|
|
||||||
const action = resolveRef(item.lookup(PDFName.of('A')));
|
|
||||||
if (action) {
|
|
||||||
dest = action.lookup(PDFName.of('D'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle named destinations
|
|
||||||
if (dest && !dest.array) {
|
|
||||||
try {
|
|
||||||
const name = dest.decodeText ? dest.decodeText() : dest.toString();
|
|
||||||
if (namedDests.has(name)) {
|
|
||||||
dest = namedDests.get(name);
|
|
||||||
} else if (dest.lookup) {
|
|
||||||
// Some named destinations resolve to a dictionary with 'D' entry
|
|
||||||
const maybeDict = resolveRef(dest);
|
|
||||||
const dictD =
|
|
||||||
maybeDict && maybeDict.lookup
|
|
||||||
? maybeDict.lookup(PDFName.of('D'))
|
|
||||||
: null;
|
|
||||||
if (dictD) dest = resolveRef(dictD);
|
|
||||||
}
|
|
||||||
} catch (_) {
|
|
||||||
// leave dest as-is if it can't be decoded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolveRef(dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
function traverse(item) {
|
|
||||||
if (!item) return null;
|
|
||||||
item = resolveRef(item);
|
|
||||||
if (!item) return null;
|
|
||||||
|
|
||||||
const title = item.lookup(PDFName.of('Title'));
|
|
||||||
const dest = getDestination(item);
|
|
||||||
const colorObj = item.lookup(PDFName.of('C'));
|
|
||||||
const flagsObj = item.lookup(PDFName.of('F'));
|
|
||||||
|
|
||||||
|
async function processOutlineItem(item) {
|
||||||
let pageIndex = 0;
|
let pageIndex = 0;
|
||||||
let destX = null;
|
let destX = null;
|
||||||
let destY = null;
|
let destY = null;
|
||||||
let zoom = null;
|
let zoom = null;
|
||||||
|
|
||||||
if (dest && dest.array) {
|
try {
|
||||||
const pageRef = dest.array[0];
|
let dest = item.dest;
|
||||||
pageIndex = findPageIndex(pageRef);
|
if (typeof dest === 'string') {
|
||||||
|
dest = await pdfJsDoc.getDestination(dest);
|
||||||
|
}
|
||||||
|
|
||||||
if (dest.array.length > 2) {
|
if (Array.isArray(dest)) {
|
||||||
const xObj = resolveRef(dest.array[2]);
|
const destRef = dest[0];
|
||||||
const yObj = resolveRef(dest.array[3]);
|
pageIndex = await pdfJsDoc.getPageIndex(destRef);
|
||||||
const zoomObj = resolveRef(dest.array[4]);
|
|
||||||
|
|
||||||
if (xObj && xObj.numberValue !== undefined) destX = xObj.numberValue;
|
if (dest.length > 2) {
|
||||||
if (yObj && yObj.numberValue !== undefined) destY = yObj.numberValue;
|
const x = dest[2];
|
||||||
if (zoomObj && zoomObj.numberValue !== undefined) {
|
const y = dest[3];
|
||||||
zoom = String(Math.round(zoomObj.numberValue * 100));
|
const z = dest[4];
|
||||||
|
|
||||||
|
if (typeof x === 'number') destX = x;
|
||||||
|
if (typeof y === 'number') destY = y;
|
||||||
|
if (typeof z === 'number') zoom = String(Math.round(z * 100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error resolving destination:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rest of the color and style processing remains the same
|
|
||||||
let color = null;
|
let color = null;
|
||||||
if (colorObj && colorObj.array) {
|
if (item.color) {
|
||||||
const [r, g, b] = colorObj.array;
|
const [r, g, b] = item.color;
|
||||||
if (r > 0.8 && g < 0.3 && b < 0.3) color = 'red';
|
const rN = r / 255;
|
||||||
else if (r < 0.3 && g < 0.3 && b > 0.8) color = 'blue';
|
const gN = g / 255;
|
||||||
else if (r < 0.3 && g > 0.8 && b < 0.3) color = 'green';
|
const bN = b / 255;
|
||||||
else if (r > 0.8 && g > 0.8 && b < 0.3) color = 'yellow';
|
|
||||||
else if (r > 0.5 && g < 0.5 && b > 0.5) color = 'purple';
|
if (rN > 0.8 && gN < 0.3 && bN < 0.3) color = 'red';
|
||||||
|
else if (rN < 0.3 && gN < 0.3 && bN > 0.8) color = 'blue';
|
||||||
|
else if (rN < 0.3 && gN > 0.8 && bN < 0.3) color = 'green';
|
||||||
|
else if (rN > 0.8 && gN > 0.8 && bN < 0.3) color = 'yellow';
|
||||||
|
else if (rN > 0.5 && gN < 0.5 && bN > 0.5) color = 'purple';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map style
|
||||||
let style = null;
|
let style = null;
|
||||||
if (flagsObj) {
|
if (item.bold && item.italic) style = 'bold-italic';
|
||||||
const flags = flagsObj.numberValue || 0;
|
else if (item.bold) style = 'bold';
|
||||||
const isBold = (flags & 2) !== 0;
|
else if (item.italic) style = 'italic';
|
||||||
const isItalic = (flags & 1) !== 0;
|
|
||||||
if (isBold && isItalic) style = 'bold-italic';
|
|
||||||
else if (isBold) style = 'bold';
|
|
||||||
else if (isItalic) style = 'italic';
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookmark = {
|
const bookmark = {
|
||||||
id: Date.now() + Math.random(),
|
id: Date.now() + Math.random(),
|
||||||
title: title ? title.decodeText() : 'Untitled',
|
title: cleanTitle(item.title),
|
||||||
page: pageIndex + 1,
|
page: pageIndex + 1,
|
||||||
children: [],
|
children: [],
|
||||||
color,
|
color,
|
||||||
@@ -2052,21 +1931,10 @@ async function extractExistingBookmarks(doc) {
|
|||||||
zoom,
|
zoom,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process children (make sure to resolve refs)
|
if (item.items && item.items.length > 0) {
|
||||||
let child = resolveRef(item.lookup(PDFName.of('First')));
|
for (const childItem of item.items) {
|
||||||
while (child) {
|
const childBookmark = await processOutlineItem(childItem);
|
||||||
const childBookmark = traverse(child);
|
bookmark.children.push(childBookmark);
|
||||||
if (childBookmark) bookmark.children.push(childBookmark);
|
|
||||||
child = resolveRef(child.lookup(PDFName.of('Next')));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageIndex === 0 && bookmark.children.length > 0) {
|
|
||||||
const firstChild = bookmark.children[0];
|
|
||||||
if (firstChild) {
|
|
||||||
bookmark.page = firstChild.page;
|
|
||||||
bookmark.destX = firstChild.destX;
|
|
||||||
bookmark.destY = firstChild.destY;
|
|
||||||
bookmark.zoom = firstChild.zoom;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2074,11 +1942,9 @@ async function extractExistingBookmarks(doc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
let first = resolveRef(outlines.lookup(PDFName.of('First')));
|
for (const item of outline) {
|
||||||
while (first) {
|
const bookmark = await processOutlineItem(item);
|
||||||
const bookmark = traverse(first);
|
result.push(bookmark);
|
||||||
if (bookmark) result.push(bookmark);
|
|
||||||
first = resolveRef(first.lookup(PDFName.of('Next')));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -2114,7 +1980,7 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
const itemDict = pdfLibDoc.context.obj({});
|
const itemDict = pdfLibDoc.context.obj({});
|
||||||
const itemRef = pdfLibDoc.context.register(itemDict);
|
const itemRef = pdfLibDoc.context.register(itemDict);
|
||||||
|
|
||||||
itemDict.set(PDFName.of('Title'), PDFString.of(node.title));
|
itemDict.set(PDFName.of('Title'), PDFHexString.fromText(node.title));
|
||||||
itemDict.set(PDFName.of('Parent'), parentRef);
|
itemDict.set(PDFName.of('Parent'), parentRef);
|
||||||
|
|
||||||
// Always map bookmark page to zero-based index consistently
|
// Always map bookmark page to zero-based index consistently
|
||||||
|
|||||||
@@ -180,7 +180,8 @@ async function performFlatteningCrop(cropData: any) {
|
|||||||
|
|
||||||
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
||||||
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
||||||
cropperState.originalPdfBytes
|
cropperState.originalPdfBytes,
|
||||||
|
{ignoreEncryption: true, throwOnInvalidObject: false}
|
||||||
);
|
);
|
||||||
const totalPages = cropperState.pdfDoc.numPages;
|
const totalPages = cropperState.pdfDoc.numPages;
|
||||||
|
|
||||||
@@ -321,7 +322,8 @@ export async function setupCropperTool() {
|
|||||||
finalPdfBytes = await newPdfDoc.save();
|
finalPdfBytes = await newPdfDoc.save();
|
||||||
} else {
|
} else {
|
||||||
const pdfToModify = await PDFLibDocument.load(
|
const pdfToModify = await PDFLibDocument.load(
|
||||||
cropperState.originalPdfBytes
|
cropperState.originalPdfBytes,
|
||||||
|
{ignoreEncryption: true, throwOnInvalidObject: false}
|
||||||
);
|
);
|
||||||
await performMetadataCrop(pdfToModify, finalCropData);
|
await performMetadataCrop(pdfToModify, finalCropData);
|
||||||
finalPdfBytes = await pdfToModify.save();
|
finalPdfBytes = await pdfToModify.save();
|
||||||
|
|||||||
@@ -68,3 +68,44 @@ export async function deletePages() {
|
|||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setupDeletePagesTool() {
|
||||||
|
const input = document.getElementById('pages-to-delete') as HTMLInputElement;
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
const updateHighlights = () => {
|
||||||
|
const val = input.value;
|
||||||
|
const pagesToDelete = new Set<number>();
|
||||||
|
|
||||||
|
const parts = val.split(',');
|
||||||
|
for (const part of parts) {
|
||||||
|
const trimmed = part.trim();
|
||||||
|
if (trimmed.includes('-')) {
|
||||||
|
const [start, end] = trimmed.split('-').map(Number);
|
||||||
|
if (!isNaN(start) && !isNaN(end) && start <= end) {
|
||||||
|
for (let i = start; i <= end; i++) pagesToDelete.add(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const num = Number(trimmed);
|
||||||
|
if (!isNaN(num)) pagesToDelete.add(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnails = document.querySelectorAll('#delete-pages-preview .page-thumbnail');
|
||||||
|
thumbnails.forEach((thumb) => {
|
||||||
|
const pageNum = parseInt((thumb as HTMLElement).dataset.pageNumber || '0');
|
||||||
|
const innerContainer = thumb.querySelector('div.relative');
|
||||||
|
|
||||||
|
if (pagesToDelete.has(pageNum)) {
|
||||||
|
innerContainer?.classList.add('border-red-500');
|
||||||
|
innerContainer?.classList.remove('border-gray-600');
|
||||||
|
} else {
|
||||||
|
innerContainer?.classList.remove('border-red-500');
|
||||||
|
innerContainer?.classList.add('border-gray-600');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
input.addEventListener('input', updateHighlights);
|
||||||
|
updateHighlights();
|
||||||
|
}
|
||||||
+69
-20
@@ -1,6 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export function flattenFormsInDoc(pdfDoc) {
|
export function flattenFormsInDoc(pdfDoc) {
|
||||||
const form = pdfDoc.getForm();
|
const form = pdfDoc.getForm();
|
||||||
@@ -8,30 +9,78 @@ export function flattenFormsInDoc(pdfDoc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function flatten() {
|
export async function flatten() {
|
||||||
if (!state.pdfDoc) {
|
if (state.files.length === 0) {
|
||||||
showAlert('Error', 'PDF not loaded.');
|
showAlert('No Files', 'Please select at least one PDF file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Flattening PDF...');
|
|
||||||
try {
|
|
||||||
flattenFormsInDoc(state.pdfDoc);
|
|
||||||
|
|
||||||
const flattenedBytes = await state.pdfDoc.save();
|
try {
|
||||||
downloadFile(
|
if (state.files.length === 1) {
|
||||||
new Blob([flattenedBytes], { type: 'application/pdf' }),
|
showLoader('Flattening PDF...');
|
||||||
'flattened.pdf'
|
const file = state.files[0];
|
||||||
);
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
|
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
flattenFormsInDoc(pdfDoc);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.includes('getForm')) {
|
||||||
|
// Ignore if no form found
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flattenedBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([flattenedBytes as any], { type: 'application/pdf' }),
|
||||||
|
`flattened_${file.name}`
|
||||||
|
);
|
||||||
|
hideLoader();
|
||||||
|
} else {
|
||||||
|
showLoader('Flattening multiple PDFs...');
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
let processedCount = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
|
const file = state.files[i];
|
||||||
|
showLoader(`Flattening ${i + 1}/${state.files.length}: ${file.name}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
|
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
flattenFormsInDoc(pdfDoc);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.includes('getForm')) {
|
||||||
|
// Ignore if no form found
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const flattenedBytes = await pdfDoc.save();
|
||||||
|
zip.file(`flattened_${file.name}`, flattenedBytes);
|
||||||
|
processedCount++;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error processing ${file.name}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedCount > 0) {
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'flattened_pdfs.zip');
|
||||||
|
showAlert('Success', `Processed ${processedCount} PDFs.`);
|
||||||
|
} else {
|
||||||
|
showAlert('Error', 'No PDFs could be processed.');
|
||||||
|
}
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if (e.message.includes('getForm')) {
|
|
||||||
showAlert(
|
|
||||||
'No Form Found',
|
|
||||||
'This PDF does not contain any form fields to flatten.'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showAlert('Error', 'Could not flatten the PDF.');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
showAlert('Error', e.message || 'An unexpected error occurred.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+307
-66
@@ -1,8 +1,9 @@
|
|||||||
import { PDFDocument, StandardFonts, rgb, TextAlignment, PDFName, PDFString, PageSizes, PDFBool, PDFDict, PDFArray } from 'pdf-lib'
|
import { PDFDocument, StandardFonts, rgb, TextAlignment, PDFName, PDFString, PageSizes, PDFBool, PDFDict, PDFArray, PDFRadioGroup } from 'pdf-lib'
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'
|
||||||
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'
|
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'
|
||||||
import { createIcons, icons } from 'lucide'
|
import { createIcons, icons } from 'lucide'
|
||||||
import * as pdfjsLib from 'pdfjs-dist'
|
import * as pdfjsLib from 'pdfjs-dist'
|
||||||
|
import 'pdfjs-dist/web/pdf_viewer.css'
|
||||||
|
|
||||||
// Initialize PDF.js worker
|
// Initialize PDF.js worker
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString()
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString()
|
||||||
@@ -49,18 +50,21 @@ interface PageData {
|
|||||||
let fields: FormField[] = []
|
let fields: FormField[] = []
|
||||||
let selectedField: FormField | null = null
|
let selectedField: FormField | null = null
|
||||||
let fieldCounter = 0
|
let fieldCounter = 0
|
||||||
|
let existingFieldNames: Set<string> = new Set()
|
||||||
|
let existingRadioGroups: Set<string> = new Set()
|
||||||
let draggedElement: HTMLElement | null = null
|
let draggedElement: HTMLElement | null = null
|
||||||
let offsetX = 0
|
let offsetX = 0
|
||||||
let offsetY = 0
|
let offsetY = 0
|
||||||
|
|
||||||
// Multi-page state
|
|
||||||
let pages: PageData[] = []
|
let pages: PageData[] = []
|
||||||
let currentPageIndex = 0
|
let currentPageIndex = 0
|
||||||
let uploadedPdfDoc: PDFDocument | null = null
|
let uploadedPdfDoc: PDFDocument | null = null
|
||||||
|
let uploadedPdfjsDoc: any = null
|
||||||
let pageSize: { width: number; height: number } = { width: 612, height: 792 }
|
let pageSize: { width: number; height: number } = { width: 612, height: 792 }
|
||||||
let currentScale = 1.333
|
let currentScale = 1.333
|
||||||
|
let pdfViewerOffset = { x: 0, y: 0 }
|
||||||
|
let pdfViewerScale = 1.333
|
||||||
|
|
||||||
// Resize state
|
|
||||||
let resizing = false
|
let resizing = false
|
||||||
let resizeField: FormField | null = null
|
let resizeField: FormField | null = null
|
||||||
let resizePos: string | null = null
|
let resizePos: string | null = null
|
||||||
@@ -419,8 +423,8 @@ function renderField(field: FormField): void {
|
|||||||
let newX = touch.clientX - rect.left - offsetX
|
let newX = touch.clientX - rect.left - offsetX
|
||||||
let newY = touch.clientY - rect.top - offsetY
|
let newY = touch.clientY - rect.top - offsetY
|
||||||
|
|
||||||
newX = Math.max(0, Math.min(newX, 816 - fieldWrapper.offsetWidth))
|
newX = Math.max(0, Math.min(newX, rect.width - fieldWrapper.offsetWidth))
|
||||||
newY = Math.max(0, Math.min(newY, 1056 - fieldWrapper.offsetHeight))
|
newY = Math.max(0, Math.min(newY, rect.height - fieldWrapper.offsetHeight))
|
||||||
|
|
||||||
fieldWrapper.style.left = newX + 'px'
|
fieldWrapper.style.left = newX + 'px'
|
||||||
fieldWrapper.style.top = newY + 'px'
|
fieldWrapper.style.top = newY + 'px'
|
||||||
@@ -496,8 +500,8 @@ document.addEventListener('mousemove', (e) => {
|
|||||||
let newX = e.clientX - rect.left - offsetX
|
let newX = e.clientX - rect.left - offsetX
|
||||||
let newY = e.clientY - rect.top - offsetY
|
let newY = e.clientY - rect.top - offsetY
|
||||||
|
|
||||||
newX = Math.max(0, Math.min(newX, 816 - draggedElement.offsetWidth))
|
newX = Math.max(0, Math.min(newX, rect.width - draggedElement.offsetWidth))
|
||||||
newY = Math.max(0, Math.min(newY, 1056 - draggedElement.offsetHeight))
|
newY = Math.max(0, Math.min(newY, rect.height - draggedElement.offsetHeight))
|
||||||
|
|
||||||
draggedElement.style.left = newX + 'px'
|
draggedElement.style.left = newX + 'px'
|
||||||
draggedElement.style.top = newY + 'px'
|
draggedElement.style.top = newY + 'px'
|
||||||
@@ -834,9 +838,21 @@ function showProperties(field: FormField): void {
|
|||||||
propertiesPanel.innerHTML = `
|
propertiesPanel.innerHTML = `
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Field Name</label>
|
<label class="block text-xs font-semibold text-gray-300 mb-1">Field Name ${field.type === 'radio' ? '(Group Name)' : ''}</label>
|
||||||
<input type="text" id="propName" value="${field.name}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
<input type="text" id="propName" value="${field.name}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
<div id="nameError" class="hidden text-red-400 text-xs mt-1"></div>
|
||||||
</div>
|
</div>
|
||||||
|
${field.type === 'radio' && (existingRadioGroups.size > 0 || fields.some(f => f.type === 'radio' && f.id !== field.id)) ? `
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-semibold text-gray-300 mb-1">Existing Radio Groups</label>
|
||||||
|
<select id="existingGroups" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||||
|
<option value="">-- Select existing group --</option>
|
||||||
|
${Array.from(existingRadioGroups).map(name => `<option value="${name}">${name}</option>`).join('')}
|
||||||
|
${Array.from(new Set(fields.filter(f => f.type === 'radio' && f.id !== field.id).map(f => f.name))).map(name => !existingRadioGroups.has(name) ? `<option value="${name}">${name}</option>` : '').join('')}
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">Select to add this button to an existing group</p>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
${specificProps}
|
${specificProps}
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Tooltip / Help Text</label>
|
<label class="block text-xs font-semibold text-gray-300 mb-1">Tooltip / Help Text</label>
|
||||||
@@ -858,25 +874,50 @@ function showProperties(field: FormField): void {
|
|||||||
|
|
||||||
// Common listeners
|
// Common listeners
|
||||||
const propName = document.getElementById('propName') as HTMLInputElement
|
const propName = document.getElementById('propName') as HTMLInputElement
|
||||||
|
const nameError = document.getElementById('nameError') as HTMLDivElement
|
||||||
const propTooltip = document.getElementById('propTooltip') as HTMLInputElement
|
const propTooltip = document.getElementById('propTooltip') as HTMLInputElement
|
||||||
const propRequired = document.getElementById('propRequired') as HTMLInputElement
|
const propRequired = document.getElementById('propRequired') as HTMLInputElement
|
||||||
const propReadOnly = document.getElementById('propReadOnly') as HTMLInputElement
|
const propReadOnly = document.getElementById('propReadOnly') as HTMLInputElement
|
||||||
const deleteBtn = document.getElementById('deleteBtn') as HTMLButtonElement
|
const deleteBtn = document.getElementById('deleteBtn') as HTMLButtonElement
|
||||||
|
|
||||||
|
const validateName = (newName: string): boolean => {
|
||||||
|
if (!newName) {
|
||||||
|
nameError.textContent = 'Field name cannot be empty'
|
||||||
|
nameError.classList.remove('hidden')
|
||||||
|
propName.classList.add('border-red-500')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === 'radio') {
|
||||||
|
nameError.classList.add('hidden')
|
||||||
|
propName.classList.remove('border-red-500')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDuplicateInFields = fields.some(f => f.id !== field.id && f.name === newName)
|
||||||
|
const isDuplicateInPdf = existingFieldNames.has(newName)
|
||||||
|
|
||||||
|
if (isDuplicateInFields || isDuplicateInPdf) {
|
||||||
|
nameError.textContent = `Field name "${newName}" already exists in this ${isDuplicateInPdf ? 'PDF' : 'form'}. Please try using a unique name.`
|
||||||
|
nameError.classList.remove('hidden')
|
||||||
|
propName.classList.add('border-red-500')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
nameError.classList.add('hidden')
|
||||||
|
propName.classList.remove('border-red-500')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
propName.addEventListener('input', (e) => {
|
||||||
|
const newName = (e.target as HTMLInputElement).value.trim()
|
||||||
|
validateName(newName)
|
||||||
|
})
|
||||||
|
|
||||||
propName.addEventListener('change', (e) => {
|
propName.addEventListener('change', (e) => {
|
||||||
const newName = (e.target as HTMLInputElement).value.trim()
|
const newName = (e.target as HTMLInputElement).value.trim()
|
||||||
|
|
||||||
if (!newName) {
|
if (!validateName(newName)) {
|
||||||
showModal('Invalid Name', 'Field name cannot be empty.', 'warning');
|
|
||||||
(e.target as HTMLInputElement).value = field.name
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate name
|
|
||||||
const isDuplicate = fields.some(f => f.id !== field.id && f.name === newName)
|
|
||||||
|
|
||||||
if (isDuplicate) {
|
|
||||||
showModal('Duplicate Name', `A field with the name "${newName}" already exists. Please choose a unique name.`, 'error');
|
|
||||||
(e.target as HTMLInputElement).value = field.name
|
(e.target as HTMLInputElement).value = field.name
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -893,6 +934,27 @@ function showProperties(field: FormField): void {
|
|||||||
field.tooltip = (e.target as HTMLInputElement).value
|
field.tooltip = (e.target as HTMLInputElement).value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (field.type === 'radio') {
|
||||||
|
const existingGroupsSelect = document.getElementById('existingGroups') as HTMLSelectElement
|
||||||
|
if (existingGroupsSelect) {
|
||||||
|
existingGroupsSelect.addEventListener('change', (e) => {
|
||||||
|
const selectedGroup = (e.target as HTMLSelectElement).value
|
||||||
|
if (selectedGroup) {
|
||||||
|
propName.value = selectedGroup
|
||||||
|
field.name = selectedGroup
|
||||||
|
validateName(selectedGroup)
|
||||||
|
|
||||||
|
// Update field label
|
||||||
|
const fieldWrapper = document.getElementById(field.id)
|
||||||
|
if (fieldWrapper) {
|
||||||
|
const label = fieldWrapper.querySelector('.field-label') as HTMLElement
|
||||||
|
if (label) label.textContent = field.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
propRequired.addEventListener('change', (e) => {
|
propRequired.addEventListener('change', (e) => {
|
||||||
field.required = (e.target as HTMLInputElement).checked
|
field.required = (e.target as HTMLInputElement).checked
|
||||||
})
|
})
|
||||||
@@ -1255,18 +1317,41 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
// Check for duplicate field names before generating PDF
|
// Check for duplicate field names before generating PDF
|
||||||
const nameCount = new Map<string, number>()
|
const nameCount = new Map<string, number>()
|
||||||
const duplicates: string[] = []
|
const duplicates: string[] = []
|
||||||
|
const conflictsWithPdf: string[] = []
|
||||||
|
|
||||||
fields.forEach(field => {
|
fields.forEach(field => {
|
||||||
const count = nameCount.get(field.name) || 0
|
const count = nameCount.get(field.name) || 0
|
||||||
nameCount.set(field.name, count + 1)
|
nameCount.set(field.name, count + 1)
|
||||||
|
|
||||||
|
if (existingFieldNames.has(field.name)) {
|
||||||
|
if (field.type === 'radio' && existingRadioGroups.has(field.name)) {
|
||||||
|
} else {
|
||||||
|
conflictsWithPdf.push(field.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
nameCount.forEach((count, name) => {
|
nameCount.forEach((count, name) => {
|
||||||
if (count > 1) {
|
if (count > 1) {
|
||||||
duplicates.push(name)
|
const fieldsWithName = fields.filter(f => f.name === name)
|
||||||
|
const allRadio = fieldsWithName.every(f => f.type === 'radio')
|
||||||
|
|
||||||
|
if (!allRadio) {
|
||||||
|
duplicates.push(name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (conflictsWithPdf.length > 0) {
|
||||||
|
const conflictList = [...new Set(conflictsWithPdf)].map(name => `"${name}"`).join(', ')
|
||||||
|
showModal(
|
||||||
|
'Field Name Conflict',
|
||||||
|
`The following field names already exist in the uploaded PDF: ${conflictList}. Please rename these fields before downloading.`,
|
||||||
|
'error'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (duplicates.length > 0) {
|
if (duplicates.length > 0) {
|
||||||
const duplicateList = duplicates.map(name => `"${name}"`).join(', ')
|
const duplicateList = duplicates.map(name => `"${name}"`).join(', ')
|
||||||
showModal(
|
showModal(
|
||||||
@@ -1318,14 +1403,24 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
const pdfPage = pdfDoc.getPage(field.pageIndex)
|
const pdfPage = pdfDoc.getPage(field.pageIndex)
|
||||||
const { height: pageHeight } = pdfPage.getSize()
|
const { height: pageHeight } = pdfPage.getSize()
|
||||||
|
|
||||||
const scaleX = 1 / currentScale
|
const scaleX = 1 / pdfViewerScale
|
||||||
const scaleY = 1 / currentScale
|
const scaleY = 1 / pdfViewerScale
|
||||||
|
|
||||||
const x = field.x * scaleX
|
const adjustedX = field.x - pdfViewerOffset.x
|
||||||
const y = pageHeight - field.y * scaleY - field.height * scaleY // PDF coordinates from bottom
|
const adjustedY = field.y - pdfViewerOffset.y
|
||||||
|
|
||||||
|
const x = adjustedX * scaleX
|
||||||
|
const y = pageHeight - (adjustedY * scaleY) - (field.height * scaleY)
|
||||||
const width = field.width * scaleX
|
const width = field.width * scaleX
|
||||||
const height = field.height * scaleY
|
const height = field.height * scaleY
|
||||||
|
|
||||||
|
console.log(`Field "${field.name}":`, {
|
||||||
|
screenPos: { x: field.x, y: field.y },
|
||||||
|
adjustedPos: { x: adjustedX, y: adjustedY },
|
||||||
|
pdfPos: { x, y, width, height },
|
||||||
|
metrics: { offset: pdfViewerOffset, scale: pdfViewerScale }
|
||||||
|
})
|
||||||
|
|
||||||
if (field.type === 'text') {
|
if (field.type === 'text') {
|
||||||
const textField = form.createTextField(field.name)
|
const textField = form.createTextField(field.name)
|
||||||
const rgbColor = hexToRgb(field.textColor)
|
const rgbColor = hexToRgb(field.textColor)
|
||||||
@@ -1398,14 +1493,23 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if (field.type === 'radio') {
|
} else if (field.type === 'radio') {
|
||||||
const groupName = field.groupName || 'RadioGroup1'
|
const groupName = field.name
|
||||||
let radioGroup
|
let radioGroup
|
||||||
|
|
||||||
if (radioGroups.has(groupName)) {
|
if (radioGroups.has(groupName)) {
|
||||||
radioGroup = radioGroups.get(groupName)
|
radioGroup = radioGroups.get(groupName)
|
||||||
} else {
|
} else {
|
||||||
radioGroup = form.createRadioGroup(groupName)
|
const existingField = form.getFieldMaybe(groupName)
|
||||||
radioGroups.set(groupName, radioGroup)
|
|
||||||
|
if (existingField) {
|
||||||
|
radioGroup = existingField
|
||||||
|
radioGroups.set(groupName, radioGroup)
|
||||||
|
console.log(`Using existing radio group from PDF: ${groupName}`)
|
||||||
|
} else {
|
||||||
|
radioGroup = form.createRadioGroup(groupName)
|
||||||
|
radioGroups.set(groupName, radioGroup)
|
||||||
|
console.log(`Created new radio group: ${groupName}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
radioGroup.addOptionToPage(field.exportValue || 'Yes', pdfPage as any, {
|
radioGroup.addOptionToPage(field.exportValue || 'Yes', pdfPage as any, {
|
||||||
@@ -1707,6 +1811,9 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
const pdfBytes = await pdfDoc.save()
|
const pdfBytes = await pdfDoc.save()
|
||||||
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' })
|
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' })
|
||||||
downloadFile(blob, 'fillable-form.pdf')
|
downloadFile(blob, 'fillable-form.pdf')
|
||||||
|
showModal('Success', 'Your PDF has been downloaded successfully.', 'info', () => {
|
||||||
|
resetToInitial()
|
||||||
|
}, 'Okay')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating PDF:', error)
|
console.error('Error generating PDF:', error)
|
||||||
const errorMessage = (error as Error).message
|
const errorMessage = (error as Error).message
|
||||||
@@ -1716,7 +1823,12 @@ downloadBtn.addEventListener('click', async () => {
|
|||||||
// Extract the field name from the error message
|
// Extract the field name from the error message
|
||||||
const match = errorMessage.match(/A field already exists with the specified name: "(.+?)"/)
|
const match = errorMessage.match(/A field already exists with the specified name: "(.+?)"/)
|
||||||
const fieldName = match ? match[1] : 'unknown'
|
const fieldName = match ? match[1] : 'unknown'
|
||||||
showModal('Duplicate Field Name', `A field named "${fieldName}" already exists. Please rename this field to use a unique name before downloading.`, 'error')
|
|
||||||
|
if (existingRadioGroups.has(fieldName)) {
|
||||||
|
console.log(`Adding to existing radio group: ${fieldName}`)
|
||||||
|
} else {
|
||||||
|
showModal('Duplicate Field Name', `A field named "${fieldName}" already exists. Please rename this field to use a unique name before downloading.`, 'error')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showModal('Error', 'Error generating PDF: ' + errorMessage, 'error')
|
showModal('Error', 'Error generating PDF: ' + errorMessage, 'error')
|
||||||
}
|
}
|
||||||
@@ -1805,7 +1917,7 @@ function switchToPage(pageIndex: number): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the canvas for the current page
|
// Render the canvas for the current page
|
||||||
function renderCanvas(): void {
|
async function renderCanvas(): Promise<void> {
|
||||||
const currentPage = pages[currentPageIndex]
|
const currentPage = pages[currentPageIndex]
|
||||||
if (!currentPage) return
|
if (!currentPage) return
|
||||||
|
|
||||||
@@ -1814,6 +1926,7 @@ function renderCanvas(): void {
|
|||||||
|
|
||||||
currentScale = scale
|
currentScale = scale
|
||||||
|
|
||||||
|
// Use actual PDF page dimensions (not scaled)
|
||||||
const canvasWidth = currentPage.width * scale
|
const canvasWidth = currentPage.width * scale
|
||||||
const canvasHeight = currentPage.height * scale
|
const canvasHeight = currentPage.height * scale
|
||||||
|
|
||||||
@@ -1822,20 +1935,135 @@ function renderCanvas(): void {
|
|||||||
|
|
||||||
canvas.innerHTML = ''
|
canvas.innerHTML = ''
|
||||||
|
|
||||||
if (currentPage.pdfPageData) {
|
if (uploadedPdfDoc) {
|
||||||
const img = document.createElement('img')
|
try {
|
||||||
img.src = currentPage.pdfPageData as string
|
const arrayBuffer = await uploadedPdfDoc.save()
|
||||||
img.style.position = 'absolute'
|
const blob = new Blob([arrayBuffer.buffer as ArrayBuffer], { type: 'application/pdf' })
|
||||||
img.style.top = '0'
|
const blobUrl = URL.createObjectURL(blob)
|
||||||
img.style.left = '0'
|
|
||||||
img.style.width = '100%'
|
const iframe = document.createElement('iframe')
|
||||||
img.style.height = '100%'
|
iframe.src = `/pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}#page=${currentPageIndex + 1}&toolbar=0`
|
||||||
img.style.pointerEvents = 'none'
|
iframe.style.width = '100%'
|
||||||
img.style.opacity = '0.8' // Slightly transparent so fields are visible
|
iframe.style.height = `${canvasHeight}px`
|
||||||
canvas.appendChild(img)
|
iframe.style.border = 'none'
|
||||||
|
iframe.style.position = 'absolute'
|
||||||
|
iframe.style.top = '0'
|
||||||
|
iframe.style.left = '0'
|
||||||
|
iframe.style.pointerEvents = 'none'
|
||||||
|
iframe.style.opacity = '0.8'
|
||||||
|
|
||||||
|
iframe.onload = () => {
|
||||||
|
try {
|
||||||
|
const viewerWindow = iframe.contentWindow as any
|
||||||
|
if (viewerWindow && viewerWindow.PDFViewerApplication) {
|
||||||
|
const app = viewerWindow.PDFViewerApplication
|
||||||
|
|
||||||
|
const style = viewerWindow.document.createElement('style')
|
||||||
|
style.textContent = `
|
||||||
|
* {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
#toolbarContainer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
#mainContainer {
|
||||||
|
top: 0 !important;
|
||||||
|
position: absolute !important;
|
||||||
|
left: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
#outerContainer {
|
||||||
|
background-color: transparent !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
#viewerContainer {
|
||||||
|
top: 0 !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.pdfViewer {
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.page {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
viewerWindow.document.head.appendChild(style)
|
||||||
|
|
||||||
|
const checkRender = setInterval(() => {
|
||||||
|
if (app.pdfViewer && app.pdfViewer.pagesCount > 0) {
|
||||||
|
clearInterval(checkRender)
|
||||||
|
|
||||||
|
const pageContainer = viewerWindow.document.querySelector('.page')
|
||||||
|
if (pageContainer) {
|
||||||
|
const initialRect = pageContainer.getBoundingClientRect()
|
||||||
|
|
||||||
|
const offsetX = -initialRect.left
|
||||||
|
const offsetY = -initialRect.top
|
||||||
|
pageContainer.style.transform = `translate(${offsetX}px, ${offsetY}px)`
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const rect = pageContainer.getBoundingClientRect()
|
||||||
|
const style = viewerWindow.getComputedStyle(pageContainer)
|
||||||
|
|
||||||
|
const borderLeft = parseFloat(style.borderLeftWidth) || 0
|
||||||
|
const borderTop = parseFloat(style.borderTopWidth) || 0
|
||||||
|
const borderRight = parseFloat(style.borderRightWidth) || 0
|
||||||
|
|
||||||
|
pdfViewerOffset = {
|
||||||
|
x: rect.left + borderLeft,
|
||||||
|
y: rect.top + borderTop
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentWidth = rect.width - borderLeft - borderRight
|
||||||
|
pdfViewerScale = contentWidth / currentPage.width
|
||||||
|
|
||||||
|
console.log('📏 Calibrated Metrics (force positioned):', {
|
||||||
|
initialPosition: { left: initialRect.left, top: initialRect.top },
|
||||||
|
appliedTransform: { x: offsetX, y: offsetY },
|
||||||
|
finalRect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
|
||||||
|
computedBorders: { left: borderLeft, top: borderTop, right: borderRight },
|
||||||
|
finalOffset: pdfViewerOffset,
|
||||||
|
finalScale: pdfViewerScale,
|
||||||
|
pdfDimensions: { width: currentPage.width, height: currentPage.height }
|
||||||
|
})
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error accessing iframe content:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.appendChild(iframe)
|
||||||
|
|
||||||
|
console.log('Canvas dimensions:', { width: canvasWidth, height: canvasHeight, scale: currentScale })
|
||||||
|
console.log('PDF page dimensions:', { width: currentPage.width, height: currentPage.height })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering PDF:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render fields for current page
|
|
||||||
fields.filter(f => f.pageIndex === currentPageIndex).forEach(field => {
|
fields.filter(f => f.pageIndex === currentPageIndex).forEach(field => {
|
||||||
renderField(field)
|
renderField(field)
|
||||||
})
|
})
|
||||||
@@ -1904,25 +2132,41 @@ async function handlePdfUpload(file: File) {
|
|||||||
uploadedPdfDoc = await PDFDocument.load(arrayBuffer)
|
uploadedPdfDoc = await PDFDocument.load(arrayBuffer)
|
||||||
|
|
||||||
// Check for existing fields and update counter
|
// Check for existing fields and update counter
|
||||||
|
existingFieldNames.clear()
|
||||||
try {
|
try {
|
||||||
const form = uploadedPdfDoc.getForm()
|
const form = uploadedPdfDoc.getForm()
|
||||||
const fields = form.getFields()
|
const pdfFields = form.getFields()
|
||||||
fields.forEach(field => {
|
|
||||||
|
// console.log('📋 Found', pdfFields.length, 'existing fields in uploaded PDF')
|
||||||
|
|
||||||
|
pdfFields.forEach(field => {
|
||||||
const name = field.getName()
|
const name = field.getName()
|
||||||
// Check if name matches pattern Type_Number
|
existingFieldNames.add(name) // Track all existing field names
|
||||||
|
|
||||||
|
if (field instanceof PDFRadioGroup) {
|
||||||
|
existingRadioGroups.add(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(' Field:', name, '| Type:', field.constructor.name)
|
||||||
|
|
||||||
const match = name.match(/([a-zA-Z]+)_(\d+)/)
|
const match = name.match(/([a-zA-Z]+)_(\d+)/)
|
||||||
if (match) {
|
if (match) {
|
||||||
const num = parseInt(match[2])
|
const num = parseInt(match[2])
|
||||||
if (!isNaN(num) && num >= fieldCounter) {
|
if (!isNaN(num) && num > fieldCounter) {
|
||||||
fieldCounter = num
|
fieldCounter = num
|
||||||
|
console.log(' → Updated field counter to:', fieldCounter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO@ALAM: DEBUGGER
|
||||||
|
// console.log('Field counter after upload:', fieldCounter)
|
||||||
|
// console.log('Existing field names:', Array.from(existingFieldNames))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No form or error getting fields, ignore
|
console.log('No form fields found or error reading fields:', e)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfjsDoc = await getPDFDocument({ data: arrayBuffer }).promise
|
uploadedPdfjsDoc = await getPDFDocument({ data: arrayBuffer }).promise
|
||||||
|
|
||||||
const pageCount = uploadedPdfDoc.getPageCount()
|
const pageCount = uploadedPdfDoc.getPageCount()
|
||||||
pages = []
|
pages = []
|
||||||
@@ -1931,27 +2175,11 @@ async function handlePdfUpload(file: File) {
|
|||||||
const page = uploadedPdfDoc.getPage(i)
|
const page = uploadedPdfDoc.getPage(i)
|
||||||
const { width, height } = page.getSize()
|
const { width, height } = page.getSize()
|
||||||
|
|
||||||
const pdfjsPage = await pdfjsDoc.getPage(i + 1)
|
|
||||||
const viewport = pdfjsPage.getViewport({ scale: 1.333 })
|
|
||||||
|
|
||||||
const tempCanvas = document.createElement('canvas')
|
|
||||||
tempCanvas.width = viewport.width
|
|
||||||
tempCanvas.height = viewport.height
|
|
||||||
|
|
||||||
const context = tempCanvas.getContext('2d')!
|
|
||||||
await pdfjsPage.render({
|
|
||||||
canvasContext: context,
|
|
||||||
viewport,
|
|
||||||
canvas: tempCanvas,
|
|
||||||
}).promise
|
|
||||||
|
|
||||||
const dataUrl = tempCanvas.toDataURL('image/png')
|
|
||||||
|
|
||||||
pages.push({
|
pages.push({
|
||||||
index: i,
|
index: i,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
pdfPageData: dataUrl as any
|
pdfPageData: undefined
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2005,17 +2233,26 @@ const errorModalTitle = document.getElementById('errorModalTitle')
|
|||||||
const errorModalMessage = document.getElementById('errorModalMessage')
|
const errorModalMessage = document.getElementById('errorModalMessage')
|
||||||
const errorModalClose = document.getElementById('errorModalClose')
|
const errorModalClose = document.getElementById('errorModalClose')
|
||||||
|
|
||||||
function showModal(title: string, message: string, type: 'error' | 'warning' | 'info' = 'error') {
|
let modalCloseCallback: (() => void) | null = null
|
||||||
if (!errorModal || !errorModalTitle || !errorModalMessage) return
|
|
||||||
|
function showModal(title: string, message: string, type: 'error' | 'warning' | 'info' = 'error', onClose?: () => void, buttonText: string = 'Close') {
|
||||||
|
if (!errorModal || !errorModalTitle || !errorModalMessage || !errorModalClose) return
|
||||||
|
|
||||||
errorModalTitle.textContent = title
|
errorModalTitle.textContent = title
|
||||||
errorModalMessage.textContent = message
|
errorModalMessage.textContent = message
|
||||||
|
errorModalClose.textContent = buttonText
|
||||||
|
|
||||||
|
modalCloseCallback = onClose || null
|
||||||
errorModal.classList.remove('hidden')
|
errorModal.classList.remove('hidden')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorModalClose) {
|
if (errorModalClose) {
|
||||||
errorModalClose.addEventListener('click', () => {
|
errorModalClose.addEventListener('click', () => {
|
||||||
errorModal?.classList.add('hidden')
|
errorModal?.classList.add('hidden')
|
||||||
|
if (modalCloseCallback) {
|
||||||
|
modalCloseCallback()
|
||||||
|
modalCloseCallback = null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2024,6 +2261,10 @@ if (errorModal) {
|
|||||||
errorModal.addEventListener('click', (e) => {
|
errorModal.addEventListener('click', (e) => {
|
||||||
if (e.target === errorModal) {
|
if (e.target === errorModal) {
|
||||||
errorModal.classList.add('hidden')
|
errorModal.classList.add('hidden')
|
||||||
|
if (modalCloseCallback) {
|
||||||
|
modalCloseCallback()
|
||||||
|
modalCloseCallback = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { pdfToPng } from './pdf-to-png.js';
|
|||||||
import { pngToPdf } from './png-to-pdf.js';
|
import { pngToPdf } from './png-to-pdf.js';
|
||||||
import { pdfToWebp } from './pdf-to-webp.js';
|
import { pdfToWebp } from './pdf-to-webp.js';
|
||||||
import { webpToPdf } from './webp-to-pdf.js';
|
import { webpToPdf } from './webp-to-pdf.js';
|
||||||
import { deletePages } from './delete-pages.js';
|
import { deletePages, setupDeletePagesTool } from './delete-pages.js';
|
||||||
import { addBlankPage } from './add-blank-page.js';
|
import { addBlankPage } from './add-blank-page.js';
|
||||||
import { extractPages } from './extract-pages.js';
|
import { extractPages } from './extract-pages.js';
|
||||||
import { addWatermark, setupWatermarkUI } from './add-watermark.js';
|
import { addWatermark, setupWatermarkUI } from './add-watermark.js';
|
||||||
@@ -67,6 +67,7 @@ import { extractAttachments } from './extract-attachments.js';
|
|||||||
import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
|
import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
|
||||||
import { sanitizePdf } from './sanitize-pdf.js';
|
import { sanitizePdf } from './sanitize-pdf.js';
|
||||||
import { removeRestrictions } from './remove-restrictions.js';
|
import { removeRestrictions } from './remove-restrictions.js';
|
||||||
|
import { repairPdf } from './repair-pdf.js';
|
||||||
|
|
||||||
export const toolLogic = {
|
export const toolLogic = {
|
||||||
merge: { process: merge, setup: setupMergeTool },
|
merge: { process: merge, setup: setupMergeTool },
|
||||||
@@ -74,6 +75,7 @@ export const toolLogic = {
|
|||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
'remove-restrictions': removeRestrictions,
|
'remove-restrictions': removeRestrictions,
|
||||||
|
'repair-pdf': repairPdf,
|
||||||
organize,
|
organize,
|
||||||
rotate,
|
rotate,
|
||||||
'add-page-numbers': addPageNumbers,
|
'add-page-numbers': addPageNumbers,
|
||||||
@@ -90,7 +92,7 @@ export const toolLogic = {
|
|||||||
'png-to-pdf': pngToPdf,
|
'png-to-pdf': pngToPdf,
|
||||||
'pdf-to-webp': pdfToWebp,
|
'pdf-to-webp': pdfToWebp,
|
||||||
'webp-to-pdf': webpToPdf,
|
'webp-to-pdf': webpToPdf,
|
||||||
'delete-pages': deletePages,
|
'delete-pages': { process: deletePages, setup: setupDeletePagesTool },
|
||||||
'add-blank-page': addBlankPage,
|
'add-blank-page': addBlankPage,
|
||||||
'extract-pages': extractPages,
|
'extract-pages': extractPages,
|
||||||
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
||||||
|
|||||||
+143
-115
@@ -1,17 +1,30 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
||||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.ts';
|
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.ts';
|
||||||
import { state } from '../state.ts';
|
import { state } from '../state.ts';
|
||||||
import { renderPagesProgressively, cleanupLazyRendering, createPlaceholder } from '../utils/render-utils.ts';
|
import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.ts';
|
||||||
|
|
||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||||
|
|
||||||
const mergeState = {
|
interface MergeState {
|
||||||
|
pdfDocs: Record<string, any>;
|
||||||
|
pdfBytes: Record<string, ArrayBuffer>;
|
||||||
|
activeMode: 'file' | 'page';
|
||||||
|
sortableInstances: {
|
||||||
|
fileList?: Sortable;
|
||||||
|
pageThumbnails?: Sortable;
|
||||||
|
};
|
||||||
|
isRendering: boolean;
|
||||||
|
cachedThumbnails: boolean | null;
|
||||||
|
lastFileHash: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeState: MergeState = {
|
||||||
pdfDocs: {},
|
pdfDocs: {},
|
||||||
|
pdfBytes: {},
|
||||||
activeMode: 'file',
|
activeMode: 'file',
|
||||||
sortableInstances: {},
|
sortableInstances: {},
|
||||||
isRendering: false,
|
isRendering: false,
|
||||||
@@ -19,47 +32,16 @@ const mergeState = {
|
|||||||
lastFileHash: null,
|
lastFileHash: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function parsePageRanges(rangeString: any, totalPages: any) {
|
const mergeWorker = new Worker('/workers/merge.worker.js');
|
||||||
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() {
|
function initializeFileListSortable() {
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
if (!fileList) return;
|
if (!fileList) return;
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
|
||||||
if (mergeState.sortableInstances.fileList) {
|
if (mergeState.sortableInstances.fileList) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
|
||||||
mergeState.sortableInstances.fileList.destroy();
|
mergeState.sortableInstances.fileList.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
|
||||||
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
|
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
|
||||||
handle: '.drag-handle',
|
handle: '.drag-handle',
|
||||||
animation: 150,
|
animation: 150,
|
||||||
@@ -79,13 +61,10 @@ function initializePageThumbnailsSortable() {
|
|||||||
const container = document.getElementById('page-merge-preview');
|
const container = document.getElementById('page-merge-preview');
|
||||||
if (!container) return;
|
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) {
|
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();
|
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, {
|
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
|
||||||
animation: 150,
|
animation: 150,
|
||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
@@ -128,13 +107,13 @@ async function renderPageMergeThumbnails() {
|
|||||||
mergeState.isRendering = true;
|
mergeState.isRendering = true;
|
||||||
container.textContent = '';
|
container.textContent = '';
|
||||||
|
|
||||||
// Cleanup any previous lazy loading observers
|
|
||||||
cleanupLazyRendering();
|
cleanupLazyRendering();
|
||||||
|
|
||||||
let totalPages = state.files.reduce((sum, file) => {
|
let totalPages = 0;
|
||||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
for (const file of state.files) {
|
||||||
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
const doc = mergeState.pdfDocs[file.name];
|
||||||
}, 0);
|
if (doc) totalPages += doc.numPages;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let currentPageNumber = 0;
|
let currentPageNumber = 0;
|
||||||
@@ -176,11 +155,8 @@ async function renderPageMergeThumbnails() {
|
|||||||
|
|
||||||
// Render pages from all files progressively
|
// Render pages from all files progressively
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
const pdfjsDoc = mergeState.pdfDocs[file.name];
|
||||||
if (!pdfDoc) continue;
|
if (!pdfjsDoc) continue;
|
||||||
|
|
||||||
const pdfData = await pdfDoc.save();
|
|
||||||
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise;
|
|
||||||
|
|
||||||
// Create a wrapper function that includes the file name
|
// Create a wrapper function that includes the file name
|
||||||
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
||||||
@@ -193,7 +169,7 @@ async function renderPageMergeThumbnails() {
|
|||||||
container,
|
container,
|
||||||
createWrapperWithFileName,
|
createWrapperWithFileName,
|
||||||
{
|
{
|
||||||
batchSize: 6,
|
batchSize: 8,
|
||||||
useLazyLoading: true,
|
useLazyLoading: true,
|
||||||
lazyLoadMargin: '300px',
|
lazyLoadMargin: '300px',
|
||||||
onProgress: (current, total) => {
|
onProgress: (current, total) => {
|
||||||
@@ -207,10 +183,6 @@ async function renderPageMergeThumbnails() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO@ALAM - DON'T destroy the PDF.js document here - lazy loading still needs it!
|
|
||||||
// It will be garbage collected automatically when no longer referenced
|
|
||||||
// pdfjsDoc.destroy(); // REMOVED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeState.cachedThumbnails = true;
|
mergeState.cachedThumbnails = true;
|
||||||
@@ -229,91 +201,159 @@ async function renderPageMergeThumbnails() {
|
|||||||
export async function merge() {
|
export async function merge() {
|
||||||
showLoader('Merging PDFs...');
|
showLoader('Merging PDFs...');
|
||||||
try {
|
try {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const jobs: MergeJob[] = [];
|
||||||
|
const filesToMerge: MergeFile[] = [];
|
||||||
|
const uniqueFileNames = new Set<string>();
|
||||||
|
|
||||||
if (mergeState.activeMode === 'file') {
|
if (mergeState.activeMode === 'file') {
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
|
if (!fileList) throw new Error('File list not found');
|
||||||
|
|
||||||
const sortedFiles = Array.from(fileList.children)
|
const sortedFiles = Array.from(fileList.children)
|
||||||
.map((li) => {
|
.map((li) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
return state.files.find((f) => f.name === (li as HTMLElement).dataset.fileName);
|
||||||
return state.files.find((f) => f.name === li.dataset.fileName);
|
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
for (const file of sortedFiles) {
|
for (const file of sortedFiles) {
|
||||||
|
if (!file) continue;
|
||||||
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||||
const rangeInput = document.getElementById(`range-${safeFileName}`);
|
const rangeInput = document.getElementById(`range-${safeFileName}`) as HTMLInputElement;
|
||||||
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
|
uniqueFileNames.add(file.name);
|
||||||
const rangeInputValue = rangeInput.value;
|
|
||||||
const sourcePdf = mergeState.pdfDocs[file.name];
|
|
||||||
if (!sourcePdf) continue;
|
|
||||||
|
|
||||||
const totalPages = sourcePdf.getPageCount();
|
if (rangeInput && rangeInput.value.trim()) {
|
||||||
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
|
jobs.push({
|
||||||
|
fileName: file.name,
|
||||||
const indicesToCopy =
|
rangeType: 'specific',
|
||||||
pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
rangeString: rangeInput.value.trim()
|
||||||
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
});
|
||||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
} else {
|
||||||
|
jobs.push({
|
||||||
|
fileName: file.name,
|
||||||
|
rangeType: 'all'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Page Mode
|
||||||
const pageContainer = document.getElementById('page-merge-preview');
|
const pageContainer = document.getElementById('page-merge-preview');
|
||||||
|
if (!pageContainer) throw new Error('Page container not found');
|
||||||
const pageElements = Array.from(pageContainer.children);
|
const pageElements = Array.from(pageContainer.children);
|
||||||
|
|
||||||
|
const rawPages: { fileName: string; pageIndex: number }[] = [];
|
||||||
for (const el of pageElements) {
|
for (const el of pageElements) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
const element = el as HTMLElement;
|
||||||
const fileName = el.dataset.fileName;
|
const fileName = element.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(element.dataset.pageIndex || '', 10); // 0-based index from dataset
|
||||||
const pageIndex = parseInt(el.dataset.pageIndex, 10);
|
|
||||||
|
|
||||||
const sourcePdf = mergeState.pdfDocs[fileName];
|
if (fileName && !isNaN(pageIndex)) {
|
||||||
if (sourcePdf && !isNaN(pageIndex)) {
|
uniqueFileNames.add(fileName);
|
||||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [
|
rawPages.push({ fileName, pageIndex });
|
||||||
pageIndex,
|
}
|
||||||
]);
|
}
|
||||||
newPdfDoc.addPage(copiedPage);
|
|
||||||
|
// Group contiguous pages
|
||||||
|
for (let i = 0; i < rawPages.length; i++) {
|
||||||
|
const current = rawPages[i];
|
||||||
|
let endPage = current.pageIndex;
|
||||||
|
|
||||||
|
while (
|
||||||
|
i + 1 < rawPages.length &&
|
||||||
|
rawPages[i + 1].fileName === current.fileName &&
|
||||||
|
rawPages[i + 1].pageIndex === endPage + 1
|
||||||
|
) {
|
||||||
|
endPage++;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage === current.pageIndex) {
|
||||||
|
// Single page
|
||||||
|
jobs.push({
|
||||||
|
fileName: current.fileName,
|
||||||
|
rangeType: 'single',
|
||||||
|
pageIndex: current.pageIndex
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Range of pages
|
||||||
|
jobs.push({
|
||||||
|
fileName: current.fileName,
|
||||||
|
rangeType: 'range',
|
||||||
|
startPage: current.pageIndex + 1,
|
||||||
|
endPage: endPage + 1
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedPdfBytes = await newPdfDoc.save();
|
if (jobs.length === 0) {
|
||||||
downloadFile(
|
showAlert('Error', 'No files or pages selected to merge.');
|
||||||
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
hideLoader();
|
||||||
'merged.pdf'
|
return;
|
||||||
);
|
}
|
||||||
showAlert('Success', 'PDFs merged successfully!');
|
|
||||||
|
for (const name of uniqueFileNames) {
|
||||||
|
const bytes = mergeState.pdfBytes[name];
|
||||||
|
if (bytes) {
|
||||||
|
filesToMerge.push({ name, data: bytes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: MergeMessage = {
|
||||||
|
command: 'merge',
|
||||||
|
files: filesToMerge,
|
||||||
|
jobs: jobs
|
||||||
|
};
|
||||||
|
|
||||||
|
mergeWorker.postMessage(message, filesToMerge.map(f => f.data));
|
||||||
|
|
||||||
|
mergeWorker.onmessage = (e: MessageEvent<MergeResponse>) => {
|
||||||
|
hideLoader();
|
||||||
|
if (e.data.status === 'success') {
|
||||||
|
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
|
||||||
|
downloadFile(blob, 'merged.pdf');
|
||||||
|
showAlert('Success', 'PDFs merged successfully!');
|
||||||
|
} else {
|
||||||
|
console.error('Worker merge error:', e.data.message);
|
||||||
|
showAlert('Error', e.data.message || 'Failed to merge PDFs.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mergeWorker.onerror = (e) => {
|
||||||
|
hideLoader();
|
||||||
|
console.error('Worker error:', e);
|
||||||
|
showAlert('Error', 'An unexpected error occurred in the merge worker.');
|
||||||
|
};
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Merge error:', e);
|
console.error('Merge error:', e);
|
||||||
showAlert(
|
showAlert(
|
||||||
'Error',
|
'Error',
|
||||||
'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
|
'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupMergeTool() {
|
export async function setupMergeTool() {
|
||||||
document.getElementById('merge-options').classList.remove('hidden');
|
document.getElementById('merge-options')?.classList.remove('hidden');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
||||||
document.getElementById('process-btn').disabled = false;
|
if (processBtn) processBtn.disabled = false;
|
||||||
|
|
||||||
const wasInPageMode = mergeState.activeMode === 'page';
|
const wasInPageMode = mergeState.activeMode === 'page';
|
||||||
|
|
||||||
showLoader('Loading PDF documents...');
|
showLoader('Loading PDF documents...');
|
||||||
try {
|
try {
|
||||||
|
mergeState.pdfDocs = {};
|
||||||
|
mergeState.pdfBytes = {};
|
||||||
|
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
if (!mergeState.pdfDocs[file.name]) {
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
mergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
|
||||||
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(
|
|
||||||
pdfBytes as ArrayBuffer,
|
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
|
||||||
{
|
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
||||||
ignoreEncryption: true,
|
mergeState.pdfDocs[file.name] = pdfjsDoc;
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDFs:', error);
|
console.error('Error loading PDFs:', error);
|
||||||
@@ -329,10 +369,12 @@ export async function setupMergeTool() {
|
|||||||
const pagePanel = document.getElementById('page-mode-panel');
|
const pagePanel = document.getElementById('page-mode-panel');
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
|
|
||||||
|
if (!fileModeBtn || !pageModeBtn || !filePanel || !pagePanel || !fileList) return;
|
||||||
|
|
||||||
fileList.textContent = ''; // Clear list safely
|
fileList.textContent = ''; // Clear list safely
|
||||||
(state.files as File[]).forEach((f) => {
|
(state.files as File[]).forEach((f) => {
|
||||||
const doc = mergeState.pdfDocs[f.name];
|
const doc = mergeState.pdfDocs[f.name];
|
||||||
const pageCount = doc ? doc.getPageCount() : 'N/A';
|
const pageCount = doc ? doc.numPages : 'N/A';
|
||||||
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||||
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
@@ -377,8 +419,8 @@ export async function setupMergeTool() {
|
|||||||
|
|
||||||
initializeFileListSortable();
|
initializeFileListSortable();
|
||||||
|
|
||||||
const newFileModeBtn = fileModeBtn.cloneNode(true);
|
const newFileModeBtn = fileModeBtn.cloneNode(true) as HTMLElement;
|
||||||
const newPageModeBtn = pageModeBtn.cloneNode(true);
|
const newPageModeBtn = pageModeBtn.cloneNode(true) as HTMLElement;
|
||||||
fileModeBtn.replaceWith(newFileModeBtn);
|
fileModeBtn.replaceWith(newFileModeBtn);
|
||||||
pageModeBtn.replaceWith(newPageModeBtn);
|
pageModeBtn.replaceWith(newPageModeBtn);
|
||||||
|
|
||||||
@@ -389,13 +431,9 @@ export async function setupMergeTool() {
|
|||||||
filePanel.classList.remove('hidden');
|
filePanel.classList.remove('hidden');
|
||||||
pagePanel.classList.add('hidden');
|
pagePanel.classList.add('hidden');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -406,13 +444,9 @@ export async function setupMergeTool() {
|
|||||||
filePanel.classList.add('hidden');
|
filePanel.classList.add('hidden');
|
||||||
pagePanel.classList.remove('hidden');
|
pagePanel.classList.remove('hidden');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
|
|
||||||
await renderPageMergeThumbnails();
|
await renderPageMergeThumbnails();
|
||||||
@@ -423,20 +457,14 @@ export async function setupMergeTool() {
|
|||||||
filePanel.classList.add('hidden');
|
filePanel.classList.add('hidden');
|
||||||
pagePanel.classList.remove('hidden');
|
pagePanel.classList.remove('hidden');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
|
|
||||||
await renderPageMergeThumbnails();
|
await renderPageMergeThumbnails();
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
|
||||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import Sortable from 'sortablejs';
|
|||||||
import { downloadFile, getPDFDocument } from '../utils/helpers';
|
import { downloadFile, getPDFDocument } from '../utils/helpers';
|
||||||
import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils';
|
import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils';
|
||||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
|
import { repairPdfFile } from './repair-pdf.js';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
@@ -351,8 +352,27 @@ async function loadPdfs(files: File[]) {
|
|||||||
if (renderCancelled) break;
|
if (renderCancelled) break;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
let arrayBuffer: ArrayBuffer;
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
|
||||||
|
try {
|
||||||
|
console.log(`Repairing ${file.name}...`);
|
||||||
|
const loadingText = document.getElementById('loading-text');
|
||||||
|
if (loadingText) loadingText.textContent = `Repairing ${file.name}...`;
|
||||||
|
|
||||||
|
const repairedData = await repairPdfFile(file);
|
||||||
|
if (repairedData) {
|
||||||
|
arrayBuffer = repairedData.buffer as ArrayBuffer;
|
||||||
|
console.log(`Successfully repaired ${file.name} before loading.`);
|
||||||
|
} else {
|
||||||
|
console.warn(`Repair returned null for ${file.name}, using original file.`);
|
||||||
|
arrayBuffer = await file.arrayBuffer();
|
||||||
|
}
|
||||||
|
} catch (repairError) {
|
||||||
|
console.warn(`Failed to repair ${file.name}, attempting to load original:`, repairError);
|
||||||
|
arrayBuffer = await file.arrayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
|
||||||
currentPdfDocs.push(pdfDoc);
|
currentPdfDocs.push(pdfDoc);
|
||||||
const pdfIndex = currentPdfDocs.length - 1;
|
const pdfIndex = currentPdfDocs.length - 1;
|
||||||
|
|
||||||
@@ -735,7 +755,7 @@ async function handleInsertPdf(e: Event) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
|
||||||
currentPdfDocs.push(pdfDoc);
|
currentPdfDocs.push(pdfDoc);
|
||||||
const pdfIndex = currentPdfDocs.length - 1;
|
const pdfIndex = currentPdfDocs.length - 1;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { repairPdf } from './repair-pdf.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { renderFileDisplay } from '../ui.js';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const fileInput = document.getElementById('file-input') as HTMLInputElement;
|
||||||
|
const dropZone = document.getElementById('drop-zone');
|
||||||
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
|
|
||||||
|
const fileControls = document.getElementById('file-controls');
|
||||||
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
|
const clearFilesBtn = document.getElementById('clear-files-btn');
|
||||||
|
const backBtn = document.getElementById('back-to-tools');
|
||||||
|
|
||||||
|
if (backBtn) {
|
||||||
|
backBtn.addEventListener('click', () => {
|
||||||
|
window.location.href = '/';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUI = () => {
|
||||||
|
if (state.files.length > 0) {
|
||||||
|
renderFileDisplay(fileDisplayArea, state.files);
|
||||||
|
if (processBtn) processBtn.classList.remove('hidden');
|
||||||
|
if (fileControls) fileControls.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
|
||||||
|
if (processBtn) processBtn.classList.add('hidden');
|
||||||
|
if (fileControls) fileControls.classList.add('hidden');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fileInput && dropZone) {
|
||||||
|
fileInput.addEventListener('change', async (e) => {
|
||||||
|
const files = (e.target as HTMLInputElement).files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
state.files = [...state.files, ...Array.from(files)];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.add('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropZone.classList.remove('bg-gray-700');
|
||||||
|
const files = e.dataTransfer?.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const pdfFiles = Array.from(files).filter(f => f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf'));
|
||||||
|
if (pdfFiles.length > 0) {
|
||||||
|
state.files = [...state.files, ...pdfFiles];
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addMoreBtn) {
|
||||||
|
addMoreBtn.addEventListener('click', () => {
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearFilesBtn) {
|
||||||
|
clearFilesBtn.addEventListener('click', () => {
|
||||||
|
state.files = [];
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.addEventListener('click', async () => {
|
||||||
|
await repairPdf();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
});
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
|
import {
|
||||||
|
downloadFile,
|
||||||
|
initializeQpdf,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
export async function repairPdfFile(file: File): Promise<Uint8Array | null> {
|
||||||
|
const inputPath = '/input.pdf';
|
||||||
|
const outputPath = '/repaired_form.pdf';
|
||||||
|
let qpdf: any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
qpdf = await initializeQpdf();
|
||||||
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
|
|
||||||
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
|
|
||||||
|
const args = [inputPath, '--decrypt', outputPath];
|
||||||
|
|
||||||
|
try {
|
||||||
|
qpdf.callMain(args);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`QPDF execution warning for ${file.name}:`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let repairedData: Uint8Array | null = null;
|
||||||
|
try {
|
||||||
|
repairedData = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to read output for ${file.name}:`, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(inputPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(outputPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Cleanup error:', cleanupError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return repairedData;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error repairing ${file.name}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function repairPdf() {
|
||||||
|
if (state.files.length === 0) {
|
||||||
|
showAlert('No Files', 'Please select one or more PDF files.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const successfulRepairs: { name: string; data: Uint8Array }[] = [];
|
||||||
|
const failedRepairs: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoader('Initializing repair engine...');
|
||||||
|
|
||||||
|
for (let i = 0; i < state.files.length; i++) {
|
||||||
|
const file = state.files[i];
|
||||||
|
showLoader(`Repairing ${file.name} (${i + 1}/${state.files.length})...`);
|
||||||
|
|
||||||
|
const repairedData = await repairPdfFile(file);
|
||||||
|
|
||||||
|
if (repairedData && repairedData.length > 0) {
|
||||||
|
successfulRepairs.push({
|
||||||
|
name: `repaired-${file.name}`,
|
||||||
|
data: repairedData,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
failedRepairs.push(file.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
if (successfulRepairs.length === 0) {
|
||||||
|
showAlert('Repair Failed', 'Unable to repair any of the uploaded PDF files.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedRepairs.length > 0) {
|
||||||
|
const failedList = failedRepairs.join(', ');
|
||||||
|
showAlert(
|
||||||
|
'Partial Success',
|
||||||
|
`Repaired ${successfulRepairs.length} file(s). Failed to repair: ${failedList}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successfulRepairs.length === 1) {
|
||||||
|
const file = successfulRepairs[0];
|
||||||
|
const blob = new Blob([file.data as any], { type: 'application/pdf' });
|
||||||
|
downloadFile(blob, file.name);
|
||||||
|
} else {
|
||||||
|
showLoader('Creating ZIP archive...');
|
||||||
|
const zip = new JSZip();
|
||||||
|
successfulRepairs.forEach((file) => {
|
||||||
|
zip.file(file.name, file.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'repaired_pdfs.zip');
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedRepairs.length === 0) {
|
||||||
|
showAlert('Success', 'All files repaired successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Critical error during repair:', error);
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', 'An unexpected error occurred during the repair process.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ export async function reversePages() {
|
|||||||
for (let j = 0; j < pdfDocs.length; j++) {
|
for (let j = 0; j < pdfDocs.length; j++) {
|
||||||
const file = pdfDocs[j];
|
const file = pdfDocs[j];
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const pageCount = pdfDoc.getPageCount();
|
const pageCount = pdfDoc.getPageCount();
|
||||||
const reversedIndices = Array.from(
|
const reversedIndices = Array.from(
|
||||||
|
|||||||
+42
-11
@@ -3,25 +3,56 @@ import { downloadFile, resetAndReloadTool } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { getRotationState, resetRotationState } from '../handlers/fileHandler.js';
|
import { getRotationState, resetRotationState } from '../handlers/fileHandler.js';
|
||||||
|
|
||||||
import { degrees } from 'pdf-lib';
|
import { PDFDocument, degrees } from 'pdf-lib';
|
||||||
|
|
||||||
export async function rotate() {
|
export async function rotate() {
|
||||||
showLoader('Applying rotations...');
|
showLoader('Applying rotations...');
|
||||||
try {
|
try {
|
||||||
const pages = state.pdfDoc.getPages();
|
const originalPdf = state.pdfDoc;
|
||||||
|
const pageCount = originalPdf.getPageCount();
|
||||||
const rotationStateArray = getRotationState();
|
const rotationStateArray = getRotationState();
|
||||||
|
|
||||||
// Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated
|
const newPdfDoc = await PDFDocument.create();
|
||||||
rotationStateArray.forEach((rotation, pageIndex) => {
|
|
||||||
if (rotation !== 0 && pages[pageIndex]) {
|
|
||||||
const currentRotation = pages[pageIndex].getRotation().angle;
|
|
||||||
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const rotatedPdfBytes = await state.pdfDoc.save();
|
for (let i = 0; i < pageCount; i++) {
|
||||||
|
const rotation = rotationStateArray[i] || 0;
|
||||||
|
const originalPage = originalPdf.getPage(i);
|
||||||
|
const currentRotation = originalPage.getRotation().angle;
|
||||||
|
const totalRotation = currentRotation + rotation;
|
||||||
|
|
||||||
|
if (totalRotation % 90 === 0) {
|
||||||
|
const [copiedPage] = await newPdfDoc.copyPages(originalPdf, [i]);
|
||||||
|
copiedPage.setRotation(degrees(totalRotation));
|
||||||
|
newPdfDoc.addPage(copiedPage);
|
||||||
|
} else {
|
||||||
|
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||||
|
const { width, height } = embeddedPage.scale(1);
|
||||||
|
|
||||||
|
const angleRad = (totalRotation * Math.PI) / 180;
|
||||||
|
const absCos = Math.abs(Math.cos(angleRad));
|
||||||
|
const absSin = Math.abs(Math.sin(angleRad));
|
||||||
|
|
||||||
|
const newWidth = width * absCos + height * absSin;
|
||||||
|
const newHeight = width * absSin + height * absCos;
|
||||||
|
|
||||||
|
const newPage = newPdfDoc.addPage([newWidth, newHeight]);
|
||||||
|
|
||||||
|
const x = newWidth / 2 - (width / 2 * Math.cos(angleRad) - height / 2 * Math.sin(angleRad));
|
||||||
|
const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad));
|
||||||
|
|
||||||
|
newPage.drawPage(embeddedPage, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
rotate: degrees(totalRotation),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotatedPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(
|
downloadFile(
|
||||||
new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
|
new Blob([rotatedPdfBytes as any], { type: 'application/pdf' }),
|
||||||
'rotated.pdf'
|
'rotated.pdf'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -80,6 +80,12 @@ const init = () => {
|
|||||||
supportSection.style.display = 'none';
|
supportSection.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide "Used by companies" section
|
||||||
|
const usedBySection = document.querySelector('.hide-section') as HTMLElement;
|
||||||
|
if (usedBySection) {
|
||||||
|
usedBySection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Hide footer but keep copyright
|
// Hide footer but keep copyright
|
||||||
const footer = document.querySelector('footer');
|
const footer = document.querySelector('footer');
|
||||||
if (footer) {
|
if (footer) {
|
||||||
@@ -291,17 +297,26 @@ const init = () => {
|
|||||||
console.log('Please share our tool and share the love!');
|
console.log('Please share our tool and share the love!');
|
||||||
|
|
||||||
|
|
||||||
const githubStarsElement = document.getElementById('github-stars');
|
const githubStarsElements = [
|
||||||
if (githubStarsElement && !__SIMPLE_MODE__) {
|
document.getElementById('github-stars-desktop'),
|
||||||
|
document.getElementById('github-stars-mobile')
|
||||||
|
];
|
||||||
|
|
||||||
|
if (githubStarsElements.some(el => el) && !__SIMPLE_MODE__) {
|
||||||
fetch('https://api.github.com/repos/alam00000/bentopdf')
|
fetch('https://api.github.com/repos/alam00000/bentopdf')
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.stargazers_count !== undefined) {
|
if (data.stargazers_count !== undefined) {
|
||||||
githubStarsElement.textContent = formatStars(data.stargazers_count);
|
const formattedStars = formatStars(data.stargazers_count);
|
||||||
|
githubStarsElements.forEach(el => {
|
||||||
|
if (el) el.textContent = formattedStars;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
githubStarsElement.textContent = '-';
|
githubStarsElements.forEach(el => {
|
||||||
|
if (el) el.textContent = '-';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+174
-40
@@ -44,19 +44,23 @@ export const dom = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const showLoader = (text = 'Processing...') => {
|
export const showLoader = (text = 'Processing...') => {
|
||||||
dom.loaderText.textContent = text;
|
if (dom.loaderText) dom.loaderText.textContent = text;
|
||||||
dom.loaderModal.classList.remove('hidden');
|
if (dom.loaderModal) dom.loaderModal.classList.remove('hidden');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hideLoader = () => dom.loaderModal.classList.add('hidden');
|
export const hideLoader = () => {
|
||||||
|
if (dom.loaderModal) dom.loaderModal.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
export const showAlert = (title: any, message: any) => {
|
export const showAlert = (title: any, message: any) => {
|
||||||
dom.alertTitle.textContent = title;
|
if (dom.alertTitle) dom.alertTitle.textContent = title;
|
||||||
dom.alertMessage.textContent = message;
|
if (dom.alertMessage) dom.alertMessage.textContent = message;
|
||||||
dom.alertModal.classList.remove('hidden');
|
if (dom.alertModal) dom.alertModal.classList.remove('hidden');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hideAlert = () => dom.alertModal.classList.add('hidden');
|
export const hideAlert = () => {
|
||||||
|
if (dom.alertModal) dom.alertModal.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
export const switchView = (view: any) => {
|
export const switchView = (view: any) => {
|
||||||
if (view === 'grid') {
|
if (view === 'grid') {
|
||||||
@@ -125,7 +129,7 @@ function initializeOrganizeSortable(containerId: any) {
|
|||||||
* @param {object} pdfDoc The loaded pdf-lib document instance.
|
* @param {object} pdfDoc The loaded pdf-lib document instance.
|
||||||
*/
|
*/
|
||||||
export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||||
const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator';
|
const containerId = toolId === 'organize' ? 'page-organizer' : toolId === 'delete-pages' ? 'delete-pages-preview' : 'page-rotator';
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
@@ -134,6 +138,9 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
|||||||
// Cleanup any previous lazy loading observers
|
// Cleanup any previous lazy loading observers
|
||||||
cleanupLazyRendering();
|
cleanupLazyRendering();
|
||||||
|
|
||||||
|
const currentRenderId = Date.now();
|
||||||
|
container.dataset.renderId = currentRenderId.toString();
|
||||||
|
|
||||||
showLoader('Rendering page previews...');
|
showLoader('Rendering page previews...');
|
||||||
|
|
||||||
const pdfData = await pdfDoc.save();
|
const pdfData = await pdfDoc.save();
|
||||||
@@ -185,7 +192,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
|||||||
|
|
||||||
wrapper.append(pageNumSpan, deleteBtn);
|
wrapper.append(pageNumSpan, deleteBtn);
|
||||||
} else if (toolId === 'rotate') {
|
} else if (toolId === 'rotate') {
|
||||||
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2';
|
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2 relative group';
|
||||||
|
|
||||||
// Read rotation from state (handles "Rotate All" on lazy-loaded pages)
|
// Read rotation from state (handles "Rotate All" on lazy-loaded pages)
|
||||||
const rotationStateArray = getRotationState();
|
const rotationStateArray = getRotationState();
|
||||||
@@ -202,35 +209,122 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
|||||||
|
|
||||||
wrapper.appendChild(imgContainer);
|
wrapper.appendChild(imgContainer);
|
||||||
|
|
||||||
const controlsDiv = document.createElement('div');
|
// Page Number Overlay (Top Left)
|
||||||
controlsDiv.className = 'flex items-center justify-center gap-3 w-full';
|
|
||||||
|
|
||||||
const pageNumSpan = document.createElement('span');
|
const pageNumSpan = document.createElement('span');
|
||||||
pageNumSpan.className = 'font-medium text-sm text-white';
|
pageNumSpan.className =
|
||||||
|
'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none';
|
||||||
pageNumSpan.textContent = pageNumber.toString();
|
pageNumSpan.textContent = pageNumber.toString();
|
||||||
|
wrapper.appendChild(pageNumSpan);
|
||||||
|
|
||||||
const rotateBtn = document.createElement('button');
|
const controlsDiv = document.createElement('div');
|
||||||
rotateBtn.className =
|
controlsDiv.className = 'flex flex-col lg:flex-row items-center justify-center w-full gap-2 px-1';
|
||||||
'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
|
|
||||||
rotateBtn.title = 'Rotate 90°';
|
// Custom Stepper Component
|
||||||
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>';
|
const stepperContainer = document.createElement('div');
|
||||||
rotateBtn.addEventListener('click', (e) => {
|
stepperContainer.className = 'flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden w-24 h-8';
|
||||||
e.stopPropagation();
|
|
||||||
const card = (e.currentTarget as HTMLElement).closest(
|
const decrementBtn = document.createElement('button');
|
||||||
'.page-rotator-item'
|
decrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center';
|
||||||
) as HTMLElement;
|
decrementBtn.innerHTML = '<i data-lucide="minus" class="w-3 h-3"></i>';
|
||||||
|
|
||||||
|
const angleInput = document.createElement('input');
|
||||||
|
angleInput.type = 'number';
|
||||||
|
angleInput.className = 'no-spinner w-full h-full bg-transparent text-white text-xs text-center focus:outline-none appearance-none m-0 p-0 border-none';
|
||||||
|
angleInput.value = initialRotation.toString();
|
||||||
|
angleInput.placeholder = "0";
|
||||||
|
|
||||||
|
const incrementBtn = document.createElement('button');
|
||||||
|
incrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center';
|
||||||
|
incrementBtn.innerHTML = '<i data-lucide="plus" class="w-3 h-3"></i>';
|
||||||
|
|
||||||
|
// Helper to update rotation
|
||||||
|
const updateRotation = (newRotation: number) => {
|
||||||
|
const card = wrapper; // Closure capture
|
||||||
const imgEl = card.querySelector('img');
|
const imgEl = card.querySelector('img');
|
||||||
const pageIndex = pageNumber - 1;
|
const pageIndex = pageNumber - 1;
|
||||||
let currentRotation = parseInt(card.dataset.rotation);
|
|
||||||
currentRotation = (currentRotation + 90) % 360;
|
|
||||||
card.dataset.rotation = currentRotation.toString();
|
|
||||||
imgEl.style.transform = `rotate(${currentRotation}deg)`;
|
|
||||||
|
|
||||||
updateRotationState(pageIndex, currentRotation);
|
// Update UI
|
||||||
|
angleInput.value = newRotation.toString();
|
||||||
|
card.dataset.rotation = newRotation.toString();
|
||||||
|
imgEl.style.transform = `rotate(${newRotation}deg)`;
|
||||||
|
|
||||||
|
// Update State
|
||||||
|
updateRotationState(pageIndex, newRotation);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event Listeners
|
||||||
|
decrementBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
let current = parseInt(angleInput.value) || 0;
|
||||||
|
updateRotation(current - 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
controlsDiv.append(pageNumSpan, rotateBtn);
|
incrementBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
let current = parseInt(angleInput.value) || 0;
|
||||||
|
updateRotation(current + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
angleInput.addEventListener('change', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
let val = parseInt((e.target as HTMLInputElement).value) || 0;
|
||||||
|
updateRotation(val);
|
||||||
|
});
|
||||||
|
angleInput.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
|
||||||
|
stepperContainer.append(decrementBtn, angleInput, incrementBtn);
|
||||||
|
|
||||||
|
const rotateBtn = document.createElement('button');
|
||||||
|
rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-1.5 rounded-md text-gray-200 transition-colors flex-shrink-0';
|
||||||
|
rotateBtn.title = 'Rotate +90°';
|
||||||
|
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
|
||||||
|
rotateBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
let current = parseInt(angleInput.value) || 0;
|
||||||
|
updateRotation(current + 90);
|
||||||
|
});
|
||||||
|
|
||||||
|
controlsDiv.append(stepperContainer, rotateBtn);
|
||||||
wrapper.appendChild(controlsDiv);
|
wrapper.appendChild(controlsDiv);
|
||||||
|
} else if (toolId === 'delete-pages') {
|
||||||
|
wrapper.className = 'page-thumbnail relative group cursor-pointer transition-all duration-200';
|
||||||
|
wrapper.dataset.pageNumber = pageNumber.toString();
|
||||||
|
|
||||||
|
const innerContainer = document.createElement('div');
|
||||||
|
innerContainer.className = 'relative w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600 transition-colors duration-200';
|
||||||
|
innerContainer.appendChild(img);
|
||||||
|
wrapper.appendChild(innerContainer);
|
||||||
|
|
||||||
|
const pageNumSpan = document.createElement('span');
|
||||||
|
pageNumSpan.className =
|
||||||
|
'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none';
|
||||||
|
pageNumSpan.textContent = pageNumber.toString();
|
||||||
|
wrapper.appendChild(pageNumSpan);
|
||||||
|
|
||||||
|
wrapper.addEventListener('click', () => {
|
||||||
|
const input = document.getElementById('pages-to-delete') as HTMLInputElement;
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
const currentVal = input.value;
|
||||||
|
let pages = currentVal.split(',').map(s => s.trim()).filter(s => s);
|
||||||
|
const pageStr = pageNumber.toString();
|
||||||
|
|
||||||
|
if (pages.includes(pageStr)) {
|
||||||
|
pages = pages.filter(p => p !== pageStr);
|
||||||
|
} else {
|
||||||
|
pages.push(pageStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.sort((a, b) => {
|
||||||
|
const numA = parseInt(a.split('-')[0]);
|
||||||
|
const numB = parseInt(b.split('-')[0]);
|
||||||
|
return numA - numB;
|
||||||
|
});
|
||||||
|
|
||||||
|
input.value = pages.join(', ');
|
||||||
|
|
||||||
|
input.dispatchEvent(new Event('input'));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
@@ -243,7 +337,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
|||||||
container,
|
container,
|
||||||
createWrapper,
|
createWrapper,
|
||||||
{
|
{
|
||||||
batchSize: 6,
|
batchSize: 8,
|
||||||
useLazyLoading: true,
|
useLazyLoading: true,
|
||||||
lazyLoadMargin: '300px',
|
lazyLoadMargin: '300px',
|
||||||
onProgress: (current, total) => {
|
onProgress: (current, total) => {
|
||||||
@@ -251,12 +345,17 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
|||||||
},
|
},
|
||||||
onBatchComplete: () => {
|
onBatchComplete: () => {
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
},
|
||||||
|
shouldCancel: () => {
|
||||||
|
return container.dataset.renderId !== currentRenderId.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (toolId === 'organize') {
|
if (toolId === 'organize') {
|
||||||
initializeOrganizeSortable(containerId);
|
initializeOrganizeSortable(containerId);
|
||||||
|
} else if (toolId === 'delete-pages') {
|
||||||
|
// No sortable needed for delete pages
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinitialize lucide icons for dynamically added elements
|
// Reinitialize lucide icons for dynamically added elements
|
||||||
@@ -576,15 +675,49 @@ export const toolTemplates = {
|
|||||||
<div id="rotate-all-controls" class="hidden my-6">
|
<div id="rotate-all-controls" class="hidden my-6">
|
||||||
<div class="bg-gray-900/50 border border-gray-700 rounded-lg p-4">
|
<div class="bg-gray-900/50 border border-gray-700 rounded-lg p-4">
|
||||||
<h3 class="text-sm font-semibold text-gray-400 mb-3 text-center">BATCH ACTIONS</h3>
|
<h3 class="text-sm font-semibold text-gray-400 mb-3 text-center">BATCH ACTIONS</h3>
|
||||||
<div class="flex justify-center gap-4">
|
<div class="flex flex-col md:flex-row justify-center gap-6 items-center">
|
||||||
<button id="rotate-all-left-btn" class="flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95">
|
|
||||||
<i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i>
|
<!-- 90 Degree Rotation Group -->
|
||||||
Rotate All Left
|
<div class="flex flex-col gap-2 items-center">
|
||||||
</button>
|
<span class="text-xs text-gray-400 font-medium uppercase tracking-wider">Rotate By 90 degrees</span>
|
||||||
<button id="rotate-all-right-btn" class="flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95">
|
<div class="flex gap-2">
|
||||||
<i data-lucide="rotate-cw" class="mr-2 h-4 w-4"></i>
|
<button id="rotate-all-left-btn" class="flex items-center justify-center px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95" title="Rotate Left 90°">
|
||||||
Rotate All Right
|
<i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i>
|
||||||
</button>
|
Left
|
||||||
|
</button>
|
||||||
|
<button id="rotate-all-right-btn" class="flex items-center justify-center px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95" title="Rotate Right 90°">
|
||||||
|
<i data-lucide="rotate-cw" class="mr-2 h-4 w-4"></i>
|
||||||
|
Right
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hidden md:block w-px h-12 bg-gray-700"></div>
|
||||||
|
|
||||||
|
<!-- Custom Rotation Group -->
|
||||||
|
<div class="flex flex-col gap-2 items-center">
|
||||||
|
<span class="text-xs text-gray-400 font-medium uppercase tracking-wider">Rotate By Custom Degrees</span>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
|
||||||
|
<!-- Custom Stepper -->
|
||||||
|
<div class="flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden h-10">
|
||||||
|
<button id="rotate-all-decrement-btn" class="px-3 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center">
|
||||||
|
<i data-lucide="minus" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
<div class="relative w-20 h-full">
|
||||||
|
<input type="number" id="custom-rotate-all-input" placeholder="0" class="no-spinner w-full h-full bg-transparent text-white text-sm text-center focus:outline-none appearance-none m-0 p-0 border-none" min="-360" max="360">
|
||||||
|
</div>
|
||||||
|
<button id="rotate-all-increment-btn" class="px-3 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center">
|
||||||
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="rotate-all-custom-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-medium px-4 py-2 rounded-lg text-sm transition-colors shadow-sm h-10">
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -814,7 +947,7 @@ export const toolTemplates = {
|
|||||||
flatten: () => `
|
flatten: () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Flatten PDF</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">Flatten PDF</h2>
|
||||||
<p class="mb-6 text-gray-400">Make PDF forms and annotations non-editable by flattening them.</p>
|
<p class="mb-6 text-gray-400">Make PDF forms and annotations non-editable by flattening them.</p>
|
||||||
${createFileInputHTML()}
|
${createFileInputHTML({ multiple: true, showControls: true })}
|
||||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
<button id="process-btn" class="hidden mt-6 btn-gradient w-full">Flatten PDF</button>
|
<button id="process-btn" class="hidden mt-6 btn-gradient w-full">Flatten PDF</button>
|
||||||
`,
|
`,
|
||||||
@@ -897,6 +1030,7 @@ export const toolTemplates = {
|
|||||||
<p class="mb-2 font-medium text-white">Total Pages: <span id="total-pages"></span></p>
|
<p class="mb-2 font-medium text-white">Total Pages: <span id="total-pages"></span></p>
|
||||||
<label for="pages-to-delete" class="block mb-2 text-sm font-medium text-gray-300">Enter pages to delete (e.g., 2, 4-6, 9):</label>
|
<label for="pages-to-delete" class="block mb-2 text-sm font-medium text-gray-300">Enter pages to delete (e.g., 2, 4-6, 9):</label>
|
||||||
<input type="text" id="pages-to-delete" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 mb-6" placeholder="e.g., 2, 4-6, 9">
|
<input type="text" id="pages-to-delete" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 mb-6" placeholder="e.g., 2, 4-6, 9">
|
||||||
|
<div id="delete-pages-preview" class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4 my-6"></div>
|
||||||
<button id="process-btn" class="btn-gradient w-full">Delete Pages & Download</button>
|
<button id="process-btn" class="btn-gradient w-full">Delete Pages & Download</button>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export interface RenderConfig {
|
|||||||
batchSize?: number;
|
batchSize?: number;
|
||||||
useLazyLoading?: boolean;
|
useLazyLoading?: boolean;
|
||||||
lazyLoadMargin?: string;
|
lazyLoadMargin?: string;
|
||||||
eagerLoadBatches?: number; // Number of batches to load ahead eagerly (default: 1)
|
eagerLoadBatches?: number; // Number of batches to load ahead eagerly (default: 2)
|
||||||
onProgress?: (current: number, total: number) => void;
|
onProgress?: (current: number, total: number) => void;
|
||||||
onPageRendered?: (pageIndex: number, element: HTMLElement) => void;
|
onPageRendered?: (pageIndex: number, element: HTMLElement) => void;
|
||||||
onBatchComplete?: () => void;
|
onBatchComplete?: () => void;
|
||||||
@@ -214,7 +214,7 @@ export async function renderPagesProgressively(
|
|||||||
const {
|
const {
|
||||||
batchSize = 8, // Increased from 5 to 8 for faster initial render
|
batchSize = 8, // Increased from 5 to 8 for faster initial render
|
||||||
useLazyLoading = true,
|
useLazyLoading = true,
|
||||||
eagerLoadBatches = 1, // Eagerly load 1 batch ahead by default
|
eagerLoadBatches = 2, // Eagerly load 1 batch ahead by default
|
||||||
onProgress,
|
onProgress,
|
||||||
onBatchComplete,
|
onBatchComplete,
|
||||||
} = config;
|
} = config;
|
||||||
@@ -318,7 +318,7 @@ export function observePlaceholder(
|
|||||||
* Eagerly renders the next batch in the background
|
* Eagerly renders the next batch in the background
|
||||||
*/
|
*/
|
||||||
function renderEagerBatch(config: RenderConfig): void {
|
function renderEagerBatch(config: RenderConfig): void {
|
||||||
const { eagerLoadBatches = 1, batchSize = 8 } = config;
|
const { eagerLoadBatches = 2, batchSize = 8 } = config;
|
||||||
|
|
||||||
if (eagerLoadBatches <= 0 || lazyLoadState.eagerLoadQueue.length === 0) {
|
if (eagerLoadBatches <= 0 || lazyLoadState.eagerLoadQueue.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { APP_VERSION } from '../../version.js';
|
||||||
|
|
||||||
|
// Handle simple mode footer replacement for tool pages
|
||||||
|
if (__SIMPLE_MODE__) {
|
||||||
|
const footer = document.querySelector('footer');
|
||||||
|
if (footer) {
|
||||||
|
footer.style.display = 'none';
|
||||||
|
|
||||||
|
const simpleFooter = document.createElement('footer');
|
||||||
|
simpleFooter.className = 'mt-16 border-t-2 border-gray-700 py-8';
|
||||||
|
simpleFooter.innerHTML = `
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<img src="../../images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
|
||||||
|
<span class="text-white font-bold text-lg">BentoPDF</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
|
Version <span id="app-version-simple">${APP_VERSION}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(simpleFooter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Add Stamps - BentoPDF</title>
|
<title>Add Stamps - BentoPDF</title>
|
||||||
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link href="../../src/css/styles.css" rel="stylesheet" />
|
<link href="../../src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
@@ -139,6 +142,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -163,7 +169,7 @@
|
|||||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<path
|
<path
|
||||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.19 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||||
@@ -187,6 +193,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
<script type="module" src="../js/utils/full-width.ts"></script>
|
<script type="module" src="../js/utils/full-width.ts"></script>
|
||||||
|
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||||
<script type="module" src="../version.ts"></script>
|
<script type="module" src="../version.ts"></script>
|
||||||
<script type="module" src="../js/logic/add-stamps.ts"></script>
|
<script type="module" src="../js/logic/add-stamps.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
|
|||||||
+20
-10
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Advanced PDF Bookmark Tool - BentoPDF</title>
|
<title>Advanced PDF Bookmark Tool - BentoPDF</title>
|
||||||
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link href="../../src/css/styles.css" rel="stylesheet" />
|
<link href="../../src/css/styles.css" rel="stylesheet" />
|
||||||
<link href="../../src/css/bookmark.css" rel="stylesheet" />
|
<link href="../../src/css/bookmark.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
@@ -271,7 +274,7 @@
|
|||||||
<input type="text" id="bookmark-title" placeholder="Bookmark title..."
|
<input type="text" id="bookmark-title" placeholder="Bookmark title..."
|
||||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg mb-2 text-sm" />
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg mb-2 text-sm" />
|
||||||
<button id="add-top-level-btn"
|
<button id="add-top-level-btn"
|
||||||
class="w-full px-3 py-2 bg-gradient-to-b from-blue-500 to-blue-600 hover:shadow-xl text-white rounded text-sm font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
|
class="w-full px-3 py-2 btn-gradient hover:shadow-xl text-white rounded text-sm font-medium !flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
|
||||||
<i data-lucide="plus" class="w-4 h-4"></i>
|
<i data-lucide="plus" class="w-4 h-4"></i>
|
||||||
<span>Add to Page <span id="current-page-display">1</span></span>
|
<span>Add to Page <span id="current-page-display">1</span></span>
|
||||||
</button>
|
</button>
|
||||||
@@ -391,11 +394,14 @@
|
|||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<button id="download-btn"
|
<button id="download-btn"
|
||||||
class="w-full px-4 py-2 bg-gradient-to-b from-blue-600 to-blue-700 hover:shadow-xl text-white rounded font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
|
class="w-full px-4 py-2 btn-gradient hover:shadow-xl text-white rounded font-medium !flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
|
||||||
Save PDF with Bookmarks
|
Save PDF with Bookmarks
|
||||||
</button>
|
</button>
|
||||||
<button id="extract-existing-btn"
|
<button id="extract-existing-btn"
|
||||||
class="w-full px-4 py-2 bg-gradient-to-b from-green-500 to-green-600 hover:shadow-xl text-white rounded font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-green-400 transition duration-200">
|
class="w-full px-4 py-2 bg-gray-50 text-gray-800
|
||||||
|
border border-gray-200 rounded-xl font-medium
|
||||||
|
flex items-center justify-center gap-2
|
||||||
|
shadow-sm hover:shadow-md transition duration-200">
|
||||||
Extract Existing Bookmarks
|
Extract Existing Bookmarks
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,7 +421,7 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">
|
<p class="text-gray-400 text-sm">
|
||||||
@@ -430,13 +436,13 @@
|
|||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li>
|
<li>
|
||||||
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
<a href="../../about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
<a href="../../faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
<a href="../../contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,10 +451,13 @@
|
|||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li>
|
<li>
|
||||||
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../../privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -492,6 +501,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
<script type="module" src="../js/utils/full-width.ts"></script>
|
<script type="module" src="../js/utils/full-width.ts"></script>
|
||||||
|
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||||
<script type="module" src="../version.ts"></script>
|
<script type="module" src="../version.ts"></script>
|
||||||
<script type="module" src="../js/logic/bookmark-pdf.ts"></script>
|
<script type="module" src="../js/logic/bookmark-pdf.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
|
|||||||
+27
-20
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Create PDF Form - BentoPDF</title>
|
<title>Create PDF Form - BentoPDF</title>
|
||||||
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link href="../../src/css/styles.css" rel="stylesheet" />
|
<link href="../../src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -298,7 +301,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
@@ -332,6 +335,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -378,27 +384,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Custom Modal -->
|
</footer>
|
||||||
<div id="errorModal"
|
<!-- Custom Modal -->
|
||||||
class="hidden fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-75">
|
<div id="errorModal" class="hidden fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-75">
|
||||||
<div class="bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 border border-gray-700">
|
<div class="bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 border border-gray-700">
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<h3 id="errorModalTitle" class="text-xl font-bold text-white mb-3">Error</h3>
|
<h3 id="errorModalTitle" class="text-xl font-bold text-white mb-3">Error</h3>
|
||||||
<p id="errorModalMessage" class="text-gray-300 mb-6">An error occurred.</p>
|
<p id="errorModalMessage" class="text-gray-300 mb-6">An error occurred.</p>
|
||||||
<button id="errorModalClose"
|
<button id="errorModalClose"
|
||||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded transition-colors">
|
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded transition-colors">
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
<script type="module" src="../js/utils/full-width.ts"></script>
|
<script type="module" src="../js/utils/full-width.ts"></script>
|
||||||
<script type="module" src="../version.ts"></script>
|
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||||
<script type="module" src="../js/logic/form-creator.ts"></script>
|
<script type="module" src="../version.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/logic/form-creator.ts"></script>
|
||||||
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>JSON to PDF Converter - BentoPDF</title>
|
<title>JSON to PDF Converter - BentoPDF</title>
|
||||||
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link href="../../src/css/styles.css" rel="stylesheet" />
|
<link href="../../src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -105,7 +108,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
@@ -139,6 +142,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -188,6 +194,7 @@
|
|||||||
<script type="module" src="../js/logic/json-to-pdf.ts"></script>
|
<script type="module" src="../js/logic/json-to-pdf.ts"></script>
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
<script type="module" src="../js/utils/full-width.ts"></script>
|
<script type="module" src="../js/utils/full-width.ts"></script>
|
||||||
|
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||||
<script type="module" src="../version.ts"></script>
|
<script type="module" src="../version.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>PDF Multi Tool - BentoPDF</title>
|
<title>PDF Multi Tool - BentoPDF</title>
|
||||||
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link href="../../src/css/styles.css" rel="stylesheet" />
|
<link href="../../src/css/styles.css" rel="stylesheet" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>PDF to JSON Converter - BentoPDF</title>
|
<title>PDF to JSON Converter - BentoPDF</title>
|
||||||
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link href="../../src/css/styles.css" rel="stylesheet" />
|
<link href="../../src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -99,7 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
@@ -133,6 +136,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -182,6 +188,7 @@
|
|||||||
<script type="module" src="../js/logic/pdf-to-json.ts"></script>
|
<script type="module" src="../js/logic/pdf-to-json.ts"></script>
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
<script type="module" src="../js/utils/full-width.ts"></script>
|
<script type="module" src="../js/utils/full-width.ts"></script>
|
||||||
|
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||||
<script type="module" src="../version.ts"></script>
|
<script type="module" src="../version.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Repair PDF - BentoPDF</title>
|
||||||
|
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
|
||||||
|
<link href="../../src/css/styles.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="antialiased bg-gray-900">
|
||||||
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="flex justify-between items-center h-16">
|
||||||
|
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||||
|
<img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="/">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
|
<div class="hidden md:flex items-center space-x-8 text-white">
|
||||||
|
<a href="/" class="nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
|
<a href="/" class="nav-link">All Tools</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Hamburger Button -->
|
||||||
|
<div class="md:hidden flex items-center">
|
||||||
|
<button id="mobile-menu-button" type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu" aria-expanded="false">
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
|
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
|
<a href="/" class="mobile-nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="/" class="mobile-nav-link">All Tools</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="uploader" class="min-h-screen flex items-center justify-center p-4 bg-gray-900">
|
||||||
|
<div id="tool-uploader"
|
||||||
|
class="bg-gray-800 rounded-xl shadow-xl p-8 max-w-2xl w-full text-gray-200 border border-gray-700">
|
||||||
|
<button id="back-to-tools"
|
||||||
|
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
|
||||||
|
<i data-lucide="arrow-left" class="cursor-pointer"></i>
|
||||||
|
<span class="cursor-pointer"> Back to Tools </span>
|
||||||
|
</button>
|
||||||
|
<h1 class="text-2xl font-bold text-white mb-2">Repair PDF</h1>
|
||||||
|
<p class="text-gray-400 mb-6">
|
||||||
|
Recover data from corrupted or damaged PDF files.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Drop Zone for Main PDF Upload -->
|
||||||
|
<div id="drop-zone"
|
||||||
|
class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700 transition-colors duration-300">
|
||||||
|
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
|
<i data-lucide="upload-cloud" class="w-10 h-10 mb-3 text-gray-400"></i>
|
||||||
|
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select a file</span> or drag and
|
||||||
|
drop</p>
|
||||||
|
<p class="text-xs text-gray-500">PDFs or Images</p>
|
||||||
|
<p class="text-xs text-gray-500">Your files never leave your device.</p>
|
||||||
|
</div>
|
||||||
|
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" multiple
|
||||||
|
accept=".pdf">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Control buttons for multi-file uploads -->
|
||||||
|
<div id="file-controls" class="hidden mt-4 flex gap-3">
|
||||||
|
<button id="add-more-btn"
|
||||||
|
class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||||
|
<i data-lucide="plus"></i> Add More Files
|
||||||
|
</button>
|
||||||
|
<button id="clear-files-btn"
|
||||||
|
class="btn bg-red-600 hover:bg-red-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||||
|
<i data-lucide="x"></i> Clear All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Display Area -->
|
||||||
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
|
|
||||||
|
<button id="process-btn" class="btn-gradient w-full mt-6 hidden">Repair PDF</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loader Modal -->
|
||||||
|
<div id="loader-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500 mb-4"></div>
|
||||||
|
<p id="loader-text" class="text-white text-lg font-semibold">Processing...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alert Modal -->
|
||||||
|
<div id="alert-modal" class="fixed inset-0 bg-gray-900 bg-opacity-90 flex items-center justify-center z-50 hidden">
|
||||||
|
<div class="bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full border border-gray-700">
|
||||||
|
<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"></p>
|
||||||
|
<button onclick="document.getElementById('alert-modal').classList.add('hidden')"
|
||||||
|
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded-lg transition-colors duration-200">
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</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>
|
||||||
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
|
Version <span id="app-version"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="../../about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../../faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../../contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="../../privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
|
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
|
||||||
|
class="text-gray-400 hover:text-indigo-400" title="GitHub">
|
||||||
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer"
|
||||||
|
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||||
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||||
|
title="Instagram">
|
||||||
|
<i data-lucide="instagram"></i>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
|
||||||
|
title="LinkedIn">
|
||||||
|
<i data-lucide="linkedin"></i>
|
||||||
|
</a>
|
||||||
|
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
|
||||||
|
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
|
<script type="module" src="../js/utils/full-width.ts"></script>
|
||||||
|
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||||
|
<script type="module" src="../version.ts"></script>
|
||||||
|
<script type="module" src="../js/logic/repair-pdf-page.ts"></script>
|
||||||
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Generate Table of Contents - BentoPDF</title>
|
<title>Generate Table of Contents - BentoPDF</title>
|
||||||
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link href="../../src/css/styles.css" rel="stylesheet" />
|
<link href="../../src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -171,7 +174,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
@@ -182,6 +185,9 @@
|
|||||||
<p class="text-gray-400 text-sm">
|
<p class="text-gray-400 text-sm">
|
||||||
© 2025 BentoPDF. All rights reserved.
|
© 2025 BentoPDF. All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
|
Version <span id="app-version"></span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -202,6 +208,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -250,6 +259,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||||
<script type="module" src="../js/utils/full-width.ts"></script>
|
<script type="module" src="../js/utils/full-width.ts"></script>
|
||||||
|
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||||
<script type="module" src="../js/logic/table-of-contents.ts"></script>
|
<script type="module" src="../js/logic/table-of-contents.ts"></script>
|
||||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+4
-1
@@ -5,7 +5,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Terms and Conditions - Bentopdf</title>
|
<title>Terms and Conditions - Bentopdf</title>
|
||||||
<link rel="icon" type="image/png" href="images/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
|
||||||
|
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||||
|
<link rel="apple-touch-icon" href="/images/favicon.png" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default defineConfig(({ mode }) => ({
|
|||||||
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
|
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
|
||||||
'add-stamps': resolve(__dirname, 'src/pages/add-stamps.html'),
|
'add-stamps': resolve(__dirname, 'src/pages/add-stamps.html'),
|
||||||
'form-creator': resolve(__dirname, 'src/pages/form-creator.html'),
|
'form-creator': resolve(__dirname, 'src/pages/form-creator.html'),
|
||||||
|
'repair-pdf': resolve(__dirname, 'src/pages/repair-pdf.html'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user