mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-05 11:52:48 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f213d9dc27 | ||
|
|
b39d2c267c | ||
|
|
09436a689d | ||
|
|
c5764e4172 | ||
|
|
2741b29ad1 | ||
|
|
05bc0d5a3c | ||
|
|
8651ca366f | ||
|
|
6b7163991e | ||
|
|
f3f6af079f | ||
|
|
6e7563d0c2 | ||
|
|
41a35586c7 |
+4
-1
@@ -13,7 +13,10 @@
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
|
||||
+4
-1
@@ -13,7 +13,10 @@
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
|
||||
+114
-28
@@ -5,7 +5,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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" />
|
||||
</head>
|
||||
|
||||
@@ -27,10 +30,38 @@
|
||||
<a href="./contact.html" class="nav-link">Contact</a>
|
||||
<a href="./licensing.html" class="nav-link">Licensing</a>
|
||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||
<a href="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>
|
||||
|
||||
<!-- 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"
|
||||
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">
|
||||
@@ -62,52 +93,106 @@
|
||||
</div>
|
||||
</nav>
|
||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||
<section id="hero-section" class="text-center py-20">
|
||||
<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">
|
||||
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
|
||||
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
|
||||
</h1>
|
||||
<p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p>
|
||||
<div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8">
|
||||
<!-- <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-12 mt-12">
|
||||
<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">
|
||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
||||
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 text-gray-400"></i>
|
||||
No Signups
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
|
||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
||||
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 text-gray-400"></i>
|
||||
Unlimited Use
|
||||
</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
|
||||
<i data-lucide="check-circle" class="w-4 h-4"></i>
|
||||
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 text-gray-400"></i>
|
||||
Works Offline
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<a href="#tools-header"
|
||||
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">Start
|
||||
Using - Forever Free</a>
|
||||
<a href="https://github.com/alam00000/bentopdf/stargazers" target="_blank" rel="noopener noreferrer" class="
|
||||
inline-flex items-center gap-1.5 text-sm font-medium
|
||||
bg-white text-gray-800 border border-gray-300
|
||||
dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600
|
||||
pl-2.5 pr-3 py-1
|
||||
rounded-full transition-colors duration-200
|
||||
shadow-sm hover:shadow-md hover:bg-gray-50 dark:hover:bg-gray-700
|
||||
">
|
||||
<svg class="w-4 h-4 flex-shrink-0 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 24 24"
|
||||
aria-hidden="true">
|
||||
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">
|
||||
Start Using Now
|
||||
<span class="bg-white text-indigo-600 rounded-full p-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
|
||||
<path d="M5 12h14" />
|
||||
<path d="m12 5 7 7-7 7" />
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</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"
|
||||
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" />
|
||||
</svg>
|
||||
|
||||
<span id="github-stars">-</span>
|
||||
</a>
|
||||
<!-- Apple -->
|
||||
<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>
|
||||
</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"
|
||||
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">
|
||||
<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"
|
||||
class="p-1 hover:bg-gray-600 rounded-md text-gray-400 hover:text-white transition-colors hidden-on-touch"
|
||||
title="Keyboard Shortcuts">
|
||||
|
||||
+15
-5
@@ -13,7 +13,10 @@
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</span>
|
||||
@@ -192,7 +195,8 @@
|
||||
<div
|
||||
class="mt-8 bg-gradient-to-r from-indigo-900/40 to-purple-900/40 rounded-lg p-6 border border-indigo-500/50">
|
||||
<div class="text-center">
|
||||
<div class="inline-block bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full mb-3 animate-pulse">
|
||||
<div
|
||||
class="inline-block bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full mb-3 animate-pulse">
|
||||
LIMITED TIME OFFER
|
||||
</div>
|
||||
<p class="text-white text-2xl font-bold mb-2">Lifetime License</p>
|
||||
@@ -202,7 +206,7 @@
|
||||
<span class="text-lg font-normal text-gray-400">one-time</span>
|
||||
</div>
|
||||
<p class="text-gray-300 text-sm mb-4">Includes all feature updates forever</p>
|
||||
|
||||
|
||||
<!-- Unlimited Usage Info -->
|
||||
<div class="bg-indigo-900/30 rounded-lg p-4 mb-4 border border-indigo-500/30">
|
||||
<div class="flex items-start gap-3 text-left">
|
||||
@@ -210,12 +214,18 @@
|
||||
<div>
|
||||
<h4 class="text-white font-semibold text-sm mb-2">Unlimited Usage</h4>
|
||||
<p class="text-gray-300 text-xs leading-relaxed">
|
||||
The BentoPDF commercial license permits use on an <strong class="text-white">unrestricted, unlimited number of devices, servers, and user machines</strong> within your organization. There are <strong class="text-white">no per-user, per-machine, or per-seat limitations</strong>. Once you purchase the license, all your internal users may use BentoPDF through your browser-based system without requiring additional licenses.
|
||||
The BentoPDF commercial license permits use on an <strong
|
||||
class="text-white">unrestricted, unlimited number of devices, servers,
|
||||
and user machines</strong> within your organization. There are <strong
|
||||
class="text-white">no per-user, per-machine, or per-seat
|
||||
limitations</strong>. Once you purchase the license, all your internal
|
||||
users may use BentoPDF through your browser-based system without requiring
|
||||
additional licenses.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<a href="https://ko-fi.com/s/f32ca4cb75" target="_blank" rel="noopener noreferrer"
|
||||
class="inline-block px-8 py-3 rounded-lg bg-indigo-600 text-white font-semibold hover:bg-indigo-700 transition-colors shadow-lg hover:shadow-indigo-500/50 transform hover:scale-105 transition-all duration-200">
|
||||
Purchase Lifetime License
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ http {
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
listen [::]:8080;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
@@ -37,4 +38,4 @@ http {
|
||||
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||
add_header Cross-Origin-Resource-Policy "cross-origin" always;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bento-pdf",
|
||||
"private": true,
|
||||
"version": "1.8.0",
|
||||
"version": "1.10.0",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+4
-1
@@ -13,7 +13,10 @@
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" />
|
||||
<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">
|
||||
<a href="index.html">BentoPDF</a>
|
||||
</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="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"/>
|
||||
</g>
|
||||
</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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.section-divider {
|
||||
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 */
|
||||
margin: 2rem auto;
|
||||
/* my-16 */
|
||||
@@ -289,9 +291,6 @@ footer a {
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(79, 70, 229, 0.3),
|
||||
0 4px 6px -4px rgba(79, 70, 229, 0.3);
|
||||
/* hover:shadow-xl hover:shadow-indigo-500/30 */
|
||||
transform: translateY(-0.25rem);
|
||||
/* hover:-translate-y-1 */
|
||||
}
|
||||
|
||||
.btn-gradient:focus {
|
||||
@@ -588,4 +587,29 @@ button:disabled,
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.bg-grid-pattern {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background-size: 60px 60px;
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
|
||||
mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
|
||||
-webkit-mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 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',
|
||||
'edit-metadata',
|
||||
'remove-metadata',
|
||||
'flatten',
|
||||
|
||||
'delete-pages',
|
||||
'add-blank-page',
|
||||
'extract-pages',
|
||||
@@ -68,4 +68,6 @@ export const multiFileTools = [
|
||||
'txt-to-pdf',
|
||||
'compress',
|
||||
'extract-attachments',
|
||||
'flatten',
|
||||
'repair-pdf',
|
||||
];
|
||||
|
||||
@@ -13,7 +13,7 @@ export const categories = [
|
||||
id: 'merge',
|
||||
name: 'Merge PDF',
|
||||
icon: 'combine',
|
||||
subtitle: 'Combine multiple PDFs into one file.',
|
||||
subtitle: 'Combine multiple PDFs into one file. Preserves Bookmarks.',
|
||||
},
|
||||
{
|
||||
id: 'split',
|
||||
@@ -310,7 +310,7 @@ export const categories = [
|
||||
id: 'alternate-merge',
|
||||
name: 'Alternate & Mix Pages',
|
||||
icon: 'shuffle',
|
||||
subtitle: 'Combine PDFs by alternating pages from each.',
|
||||
subtitle: 'Merge PDFs by alternating pages from each PDF. Preserves Bookmarks',
|
||||
},
|
||||
{
|
||||
id: 'organize',
|
||||
@@ -468,6 +468,12 @@ export const categories = [
|
||||
subtitle:
|
||||
'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();
|
||||
}
|
||||
|
||||
if (toolId === 'organize' || toolId === 'rotate') {
|
||||
if (toolId === 'organize' || toolId === 'rotate' || toolId === 'delete-pages') {
|
||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||
|
||||
if (toolId === 'rotate') {
|
||||
@@ -134,14 +134,19 @@ async function handleSinglePdfUpload(toolId, file) {
|
||||
const rotateAllRightBtn = document.getElementById(
|
||||
'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');
|
||||
createIcons({ icons });
|
||||
|
||||
const rotateAll = (direction) => {
|
||||
const rotateAll = (angle: number) => {
|
||||
// Update rotation state for ALL pages (including unrendered ones)
|
||||
for (let i = 0; i < rotationState.length; i++) {
|
||||
rotationState[i] = (rotationState[i] + direction * 90 + 360) % 360;
|
||||
rotationState[i] = (rotationState[i] + angle);
|
||||
}
|
||||
|
||||
// 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 newRotation = rotationState[pageIndex];
|
||||
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||
|
||||
const thumbnail = item.querySelector('canvas, img');
|
||||
if (thumbnail) {
|
||||
(thumbnail as HTMLElement).style.transform =
|
||||
`rotate(${newRotation}deg)`;
|
||||
}
|
||||
|
||||
const input = item.querySelector('input');
|
||||
if (input) {
|
||||
input.value = newRotation.toString();
|
||||
}
|
||||
});
|
||||
};
|
||||
rotateAllLeftBtn.onclick = () => rotateAll(-1);
|
||||
rotateAllRightBtn.onclick = () => rotateAll(1);
|
||||
rotateAllLeftBtn.onclick = () => rotateAll(-90);
|
||||
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 (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');
|
||||
if (processBtn) {
|
||||
(processBtn as HTMLButtonElement).disabled = false;
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
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 { PDFDocument } from 'pdf-lib';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
const alternateMergeState = {
|
||||
interface AlternateMergeState {
|
||||
pdfDocs: Record<string, any>;
|
||||
pdfBytes: Record<string, ArrayBuffer>;
|
||||
}
|
||||
|
||||
const alternateMergeState: AlternateMergeState = {
|
||||
pdfDocs: {},
|
||||
pdfBytes: {},
|
||||
};
|
||||
|
||||
const alternateMergeWorker = new Worker('/workers/alternate-merge.worker.js');
|
||||
|
||||
export async function setupAlternateMergeTool() {
|
||||
const optionsDiv = document.getElementById('alternate-merge-options');
|
||||
const processBtn = document.getElementById(
|
||||
@@ -23,18 +30,18 @@ export async function setupAlternateMergeTool() {
|
||||
|
||||
fileList.innerHTML = '';
|
||||
alternateMergeState.pdfDocs = {};
|
||||
alternateMergeState.pdfBytes = {};
|
||||
|
||||
showLoader('Loading PDF documents...');
|
||||
try {
|
||||
for (const file of state.files) {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(
|
||||
pdfBytes as ArrayBuffer,
|
||||
{
|
||||
ignoreEncryption: true,
|
||||
}
|
||||
);
|
||||
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
|
||||
alternateMergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
|
||||
|
||||
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
|
||||
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
||||
alternateMergeState.pdfDocs[file.name] = pdfjsDoc;
|
||||
const pageCount = pdfjsDoc.numPages;
|
||||
|
||||
const li = document.createElement('li');
|
||||
li.className =
|
||||
@@ -79,7 +86,7 @@ export async function setupAlternateMergeTool() {
|
||||
}
|
||||
|
||||
export async function alternateMerge() {
|
||||
if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
|
||||
if (Object.keys(alternateMergeState.pdfBytes).length < 2) {
|
||||
showAlert(
|
||||
'Not Enough Files',
|
||||
'Please upload at least two PDF files to alternate and mix.'
|
||||
@@ -89,37 +96,55 @@ export async function alternateMerge() {
|
||||
|
||||
showLoader('Alternating and mixing pages...');
|
||||
try {
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
const fileList = document.getElementById('alternate-file-list');
|
||||
if (!fileList) throw new Error('File list not found');
|
||||
|
||||
const sortedFileNames = Array.from(fileList.children).map(
|
||||
(li) => (li as HTMLElement).dataset.fileName
|
||||
);
|
||||
).filter(Boolean) as string[];
|
||||
|
||||
const loadedDocs = sortedFileNames.map(
|
||||
(name) => alternateMergeState.pdfDocs[name]
|
||||
);
|
||||
const pageCounts = loadedDocs.map((doc) => doc.getPageCount());
|
||||
const maxPages = Math.max(...pageCounts);
|
||||
|
||||
for (let i = 0; i < maxPages; i++) {
|
||||
for (const doc of loadedDocs) {
|
||||
if (i < doc.getPageCount()) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
}
|
||||
const filesToMerge: InterleaveFile[] = [];
|
||||
for (const name of sortedFileNames) {
|
||||
const bytes = alternateMergeState.pdfBytes[name];
|
||||
if (bytes) {
|
||||
filesToMerge.push({ name, data: bytes });
|
||||
}
|
||||
}
|
||||
|
||||
const mergedPdfBytes = await newPdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
||||
'alternated-mixed.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDFs have been mixed successfully!');
|
||||
if (filesToMerge.length < 2) {
|
||||
showAlert('Error', 'At least two valid PDFs are required.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('Alternate Merge error:', e);
|
||||
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
+188
-322
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
// 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 Sortable from 'sortablejs';
|
||||
import { createIcons, icons } from 'lucide';
|
||||
@@ -37,19 +37,19 @@ function showInputModal(title, fields = [], defaultValues = {}) {
|
||||
.map((field) => {
|
||||
if (field.type === 'text') {
|
||||
return `
|
||||
<div class="mb-4">
|
||||
<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] || '')}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
placeholder="${field.placeholder || ''}" />
|
||||
</div>
|
||||
`;
|
||||
<div class="mb-4">
|
||||
<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] || '')}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg"
|
||||
placeholder="${field.placeholder || ''}" />
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'select') {
|
||||
return `
|
||||
<div class="mb-4">
|
||||
<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">
|
||||
${field.options
|
||||
<div class="mb-4">
|
||||
<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">
|
||||
${field.options
|
||||
.map(
|
||||
(opt) => `
|
||||
<option value="${opt.value}" ${defaultValues[field.name] === opt.value ? 'selected' : ''}>
|
||||
@@ -57,91 +57,92 @@ function showInputModal(title, fields = [], defaultValues = {}) {
|
||||
</option>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</select>
|
||||
.join('')
|
||||
}
|
||||
</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" />' : ''}
|
||||
</div>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'destination') {
|
||||
const hasDestination =
|
||||
defaultValues.destX !== null && defaultValues.destX !== undefined;
|
||||
return `
|
||||
<div class="mb-4">
|
||||
<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="flex items-center gap-2">
|
||||
<label class="flex items-center gap-1 text-xs">
|
||||
<input type="checkbox" id="modal-use-destination" class="w-4 h-4" ${hasDestination ? 'checked' : ''}>
|
||||
<span>Set custom destination</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="destination-controls" class="${hasDestination ? '' : 'hidden'} space-y-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<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}"
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" step="1" />
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
<option value="">Inherit</option>
|
||||
<option value="0">Fit Page</option>
|
||||
<option value="50">50%</option>
|
||||
<option value="75">75%</option>
|
||||
<option value="100">100%</option>
|
||||
<option value="125">125%</option>
|
||||
<option value="150">150%</option>
|
||||
<option value="200">200%</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="text-xs text-gray-600">X Position</label>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-600">Y Position</label>
|
||||
<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" />
|
||||
</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">
|
||||
<i data-lucide="crosshair" class="w-3 h-3"></i> Click on PDF to Pick Location
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div class="mb-4">
|
||||
<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="flex items-center gap-2">
|
||||
<label class="flex items-center gap-1 text-xs">
|
||||
<input type="checkbox" id="modal-use-destination" class="w-4 h-4" ${hasDestination ? 'checked' : ''}>
|
||||
<span>Set custom destination </span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="destination-controls" class="${hasDestination ? '' : 'hidden'} space-y-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<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}"
|
||||
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" step="1" />
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
<option value="">Inherit</option>
|
||||
<option value="0">Fit Page</option>
|
||||
<option value="50">50%</option>
|
||||
<option value="75">75%</option>
|
||||
<option value="100">100%</option>
|
||||
<option value="125">125%</option>
|
||||
<option value="150">150%</option>
|
||||
<option value="200">200%</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="text-xs text-gray-600">X Position</label>
|
||||
<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" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-600">Y Position</label>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (field.type === 'preview') {
|
||||
return `
|
||||
<div class="mb-4">
|
||||
<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">
|
||||
<span id="preview-text" style="font-size: 16px;">Preview Text</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div class="mb-4">
|
||||
<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">
|
||||
<span id="preview-text" style="font-size: 16px;">Preview Text</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.join('');
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
|
||||
<div class="mb-6">
|
||||
${fieldsHTML}
|
||||
</div>
|
||||
<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-confirm" class="px-4 py-2 rounded bg-blue-500 hover:bg-blue-600 text-white">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
|
||||
<div class="mb-6">
|
||||
${fieldsHTML}
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<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 btn-gradient text-white">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(modal);
|
||||
modalContainer.appendChild(overlay);
|
||||
@@ -476,7 +477,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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.top = e.clientY - rect.top + 15 + 'px';
|
||||
});
|
||||
@@ -525,12 +526,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
destinationMarker = document.createElement('div');
|
||||
destinationMarker.className = 'destination-marker';
|
||||
destinationMarker.innerHTML = `
|
||||
<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"/>
|
||||
<path d="M12 2 L12 22 M2 12 L22 12"/>
|
||||
<circle cx="12" cy="12" r="2" fill="#3b82f6"/>
|
||||
</svg>
|
||||
`;
|
||||
<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" />
|
||||
<path d="M12 2 L12 22 M2 12 L22 12" />
|
||||
<circle cx="12" cy="12" r="2" fill="#3b82f6" />
|
||||
</svg>
|
||||
`;
|
||||
const canvasRect = canvas.getBoundingClientRect();
|
||||
const wrapperRect = canvasWrapper.getBoundingClientRect();
|
||||
destinationMarker.style.position = 'absolute';
|
||||
@@ -549,7 +550,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
canvasX + canvasRect.left - wrapperRect.left + 20 + 'px';
|
||||
coordDisplay.style.top =
|
||||
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);
|
||||
|
||||
// Call callback with PDF coordinates
|
||||
@@ -577,14 +578,14 @@ function showConfirmModal(message) {
|
||||
modal.className = 'modal-content';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">Confirm Action</h3>
|
||||
<p class="text-gray-600 mb-6">${message}</p>
|
||||
<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-confirm" class="px-4 py-2 rounded bg-blue-500 hover:bg-blue-600 text-white">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">Confirm Action</h3>
|
||||
<p class="text-gray-600 mb-6">${message}</p>
|
||||
<div class="flex gap-2 justify-end">
|
||||
<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 btn-gradient text-white">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(modal);
|
||||
@@ -618,14 +619,14 @@ function showAlertModal(title, message) {
|
||||
modal.className = 'modal-content';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
|
||||
<p class="text-gray-600 mb-6">${message}</p>
|
||||
<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>
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
|
||||
<p class="text-gray-600 mb-6">${message}</p>
|
||||
<div class="flex justify-end">
|
||||
<button id="modal-ok" class="px-4 py-2 rounded btn-gradient text-white">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(modal);
|
||||
modalContainer.appendChild(overlay);
|
||||
@@ -832,7 +833,7 @@ deleteAllBtn.addEventListener('click', async () => {
|
||||
}
|
||||
|
||||
const confirmed = await showConfirmModal(
|
||||
`Delete all ${bookmarkTree.length} bookmark(s)?`
|
||||
`Delete all ${bookmarkTree.length} bookmark(s) ? `
|
||||
);
|
||||
if (confirmed) {
|
||||
bookmarkTree = [];
|
||||
@@ -949,7 +950,7 @@ batchDeleteBtn.addEventListener('click', async () => {
|
||||
if (selectedBookmarks.size === 0) return;
|
||||
|
||||
const confirmed = await showConfirmModal(
|
||||
`Delete ${selectedBookmarks.size} bookmark(s)?`
|
||||
`Delete ${selectedBookmarks.size} bookmark(s) ? `
|
||||
);
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -1173,7 +1174,7 @@ async function renderPage(num, zoom = null, destX = null, destY = null) {
|
||||
ctx.stroke();
|
||||
|
||||
// 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';
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const textWidth = textMetrics.width;
|
||||
@@ -1532,7 +1533,7 @@ function createNodeElement(node, level = 0) {
|
||||
if (result && result.title) {
|
||||
node.children.push({
|
||||
id: Date.now() + Math.random(),
|
||||
title: result.title,
|
||||
title: cleanTitle(result.title),
|
||||
page: currentPage,
|
||||
children: [],
|
||||
color: null,
|
||||
@@ -1608,7 +1609,7 @@ function createNodeElement(node, level = 0) {
|
||||
);
|
||||
|
||||
if (result) {
|
||||
node.title = result.title;
|
||||
node.title = cleanTitle(result.title);
|
||||
node.color = result.color || null;
|
||||
node.style = result.style || null;
|
||||
|
||||
@@ -1756,7 +1757,7 @@ function parseCSV(text) {
|
||||
const [, title, page, level] = match;
|
||||
const bookmark = {
|
||||
id: Date.now() + Math.random(),
|
||||
title: title.replace(/""/g, '"'),
|
||||
title: cleanTitle(title.replace(/""/g, '"')),
|
||||
page: parseInt(page),
|
||||
children: [],
|
||||
color: null,
|
||||
@@ -1787,6 +1788,15 @@ jsonImportHidden.addEventListener('change', async (e) => {
|
||||
const text = await file.text();
|
||||
try {
|
||||
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;
|
||||
saveState();
|
||||
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) {
|
||||
try {
|
||||
const outlines = doc.catalog.lookup(PDFName.of('Outlines'));
|
||||
if (!outlines) 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'));
|
||||
const outline = await pdfJsDoc.getOutline();
|
||||
console.log(outline);
|
||||
if (!outline) return [];
|
||||
|
||||
async function processOutlineItem(item) {
|
||||
let pageIndex = 0;
|
||||
let destX = null;
|
||||
let destY = null;
|
||||
let zoom = null;
|
||||
|
||||
if (dest && dest.array) {
|
||||
const pageRef = dest.array[0];
|
||||
pageIndex = findPageIndex(pageRef);
|
||||
try {
|
||||
let dest = item.dest;
|
||||
if (typeof dest === 'string') {
|
||||
dest = await pdfJsDoc.getDestination(dest);
|
||||
}
|
||||
|
||||
if (dest.array.length > 2) {
|
||||
const xObj = resolveRef(dest.array[2]);
|
||||
const yObj = resolveRef(dest.array[3]);
|
||||
const zoomObj = resolveRef(dest.array[4]);
|
||||
if (Array.isArray(dest)) {
|
||||
const destRef = dest[0];
|
||||
pageIndex = await pdfJsDoc.getPageIndex(destRef);
|
||||
|
||||
if (xObj && xObj.numberValue !== undefined) destX = xObj.numberValue;
|
||||
if (yObj && yObj.numberValue !== undefined) destY = yObj.numberValue;
|
||||
if (zoomObj && zoomObj.numberValue !== undefined) {
|
||||
zoom = String(Math.round(zoomObj.numberValue * 100));
|
||||
if (dest.length > 2) {
|
||||
const x = dest[2];
|
||||
const y = dest[3];
|
||||
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;
|
||||
if (colorObj && colorObj.array) {
|
||||
const [r, g, b] = colorObj.array;
|
||||
if (r > 0.8 && g < 0.3 && b < 0.3) color = 'red';
|
||||
else if (r < 0.3 && g < 0.3 && b > 0.8) color = 'blue';
|
||||
else if (r < 0.3 && g > 0.8 && b < 0.3) color = 'green';
|
||||
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 (item.color) {
|
||||
const [r, g, b] = item.color;
|
||||
const rN = r / 255;
|
||||
const gN = g / 255;
|
||||
const bN = b / 255;
|
||||
|
||||
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;
|
||||
if (flagsObj) {
|
||||
const flags = flagsObj.numberValue || 0;
|
||||
const isBold = (flags & 2) !== 0;
|
||||
const isItalic = (flags & 1) !== 0;
|
||||
if (isBold && isItalic) style = 'bold-italic';
|
||||
else if (isBold) style = 'bold';
|
||||
else if (isItalic) style = 'italic';
|
||||
}
|
||||
if (item.bold && item.italic) style = 'bold-italic';
|
||||
else if (item.bold) style = 'bold';
|
||||
else if (item.italic) style = 'italic';
|
||||
|
||||
const bookmark = {
|
||||
id: Date.now() + Math.random(),
|
||||
title: title ? title.decodeText() : 'Untitled',
|
||||
title: cleanTitle(item.title),
|
||||
page: pageIndex + 1,
|
||||
children: [],
|
||||
color,
|
||||
@@ -2052,21 +1931,10 @@ async function extractExistingBookmarks(doc) {
|
||||
zoom,
|
||||
};
|
||||
|
||||
// Process children (make sure to resolve refs)
|
||||
let child = resolveRef(item.lookup(PDFName.of('First')));
|
||||
while (child) {
|
||||
const childBookmark = traverse(child);
|
||||
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;
|
||||
if (item.items && item.items.length > 0) {
|
||||
for (const childItem of item.items) {
|
||||
const childBookmark = await processOutlineItem(childItem);
|
||||
bookmark.children.push(childBookmark);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2074,11 +1942,9 @@ async function extractExistingBookmarks(doc) {
|
||||
}
|
||||
|
||||
const result = [];
|
||||
let first = resolveRef(outlines.lookup(PDFName.of('First')));
|
||||
while (first) {
|
||||
const bookmark = traverse(first);
|
||||
if (bookmark) result.push(bookmark);
|
||||
first = resolveRef(first.lookup(PDFName.of('Next')));
|
||||
for (const item of outline) {
|
||||
const bookmark = await processOutlineItem(item);
|
||||
result.push(bookmark);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -2114,7 +1980,7 @@ downloadBtn.addEventListener('click', async () => {
|
||||
const itemDict = pdfLibDoc.context.obj({});
|
||||
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);
|
||||
|
||||
// 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
|
||||
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
||||
cropperState.originalPdfBytes
|
||||
cropperState.originalPdfBytes,
|
||||
{ignoreEncryption: true, throwOnInvalidObject: false}
|
||||
);
|
||||
const totalPages = cropperState.pdfDoc.numPages;
|
||||
|
||||
@@ -321,7 +322,8 @@ export async function setupCropperTool() {
|
||||
finalPdfBytes = await newPdfDoc.save();
|
||||
} else {
|
||||
const pdfToModify = await PDFLibDocument.load(
|
||||
cropperState.originalPdfBytes
|
||||
cropperState.originalPdfBytes,
|
||||
{ignoreEncryption: true, throwOnInvalidObject: false}
|
||||
);
|
||||
await performMetadataCrop(pdfToModify, finalCropData);
|
||||
finalPdfBytes = await pdfToModify.save();
|
||||
|
||||
@@ -68,3 +68,44 @@ export async function deletePages() {
|
||||
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 { downloadFile } from '../utils/helpers.js';
|
||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
export function flattenFormsInDoc(pdfDoc) {
|
||||
const form = pdfDoc.getForm();
|
||||
@@ -8,30 +9,78 @@ export function flattenFormsInDoc(pdfDoc) {
|
||||
}
|
||||
|
||||
export async function flatten() {
|
||||
if (!state.pdfDoc) {
|
||||
showAlert('Error', 'PDF not loaded.');
|
||||
if (state.files.length === 0) {
|
||||
showAlert('No Files', 'Please select at least one PDF file.');
|
||||
return;
|
||||
}
|
||||
showLoader('Flattening PDF...');
|
||||
try {
|
||||
flattenFormsInDoc(state.pdfDoc);
|
||||
|
||||
const flattenedBytes = await state.pdfDoc.save();
|
||||
downloadFile(
|
||||
new Blob([flattenedBytes], { type: 'application/pdf' }),
|
||||
'flattened.pdf'
|
||||
);
|
||||
try {
|
||||
if (state.files.length === 1) {
|
||||
showLoader('Flattening 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) {
|
||||
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();
|
||||
showAlert('Error', e.message || 'An unexpected error occurred.');
|
||||
}
|
||||
}
|
||||
|
||||
+308
-67
@@ -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 { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'
|
||||
import { createIcons, icons } from 'lucide'
|
||||
import * as pdfjsLib from 'pdfjs-dist'
|
||||
import 'pdfjs-dist/web/pdf_viewer.css'
|
||||
|
||||
// Initialize PDF.js worker
|
||||
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 selectedField: FormField | null = null
|
||||
let fieldCounter = 0
|
||||
let existingFieldNames: Set<string> = new Set()
|
||||
let existingRadioGroups: Set<string> = new Set()
|
||||
let draggedElement: HTMLElement | null = null
|
||||
let offsetX = 0
|
||||
let offsetY = 0
|
||||
|
||||
// Multi-page state
|
||||
let pages: PageData[] = []
|
||||
let currentPageIndex = 0
|
||||
let uploadedPdfDoc: PDFDocument | null = null
|
||||
let uploadedPdfjsDoc: any = null
|
||||
let pageSize: { width: number; height: number } = { width: 612, height: 792 }
|
||||
let currentScale = 1.333
|
||||
let pdfViewerOffset = { x: 0, y: 0 }
|
||||
let pdfViewerScale = 1.333
|
||||
|
||||
// Resize state
|
||||
let resizing = false
|
||||
let resizeField: FormField | null = null
|
||||
let resizePos: string | null = null
|
||||
@@ -419,8 +423,8 @@ function renderField(field: FormField): void {
|
||||
let newX = touch.clientX - rect.left - offsetX
|
||||
let newY = touch.clientY - rect.top - offsetY
|
||||
|
||||
newX = Math.max(0, Math.min(newX, 816 - fieldWrapper.offsetWidth))
|
||||
newY = Math.max(0, Math.min(newY, 1056 - fieldWrapper.offsetHeight))
|
||||
newX = Math.max(0, Math.min(newX, rect.width - fieldWrapper.offsetWidth))
|
||||
newY = Math.max(0, Math.min(newY, rect.height - fieldWrapper.offsetHeight))
|
||||
|
||||
fieldWrapper.style.left = newX + 'px'
|
||||
fieldWrapper.style.top = newY + 'px'
|
||||
@@ -496,8 +500,8 @@ document.addEventListener('mousemove', (e) => {
|
||||
let newX = e.clientX - rect.left - offsetX
|
||||
let newY = e.clientY - rect.top - offsetY
|
||||
|
||||
newX = Math.max(0, Math.min(newX, 816 - draggedElement.offsetWidth))
|
||||
newY = Math.max(0, Math.min(newY, 1056 - draggedElement.offsetHeight))
|
||||
newX = Math.max(0, Math.min(newX, rect.width - draggedElement.offsetWidth))
|
||||
newY = Math.max(0, Math.min(newY, rect.height - draggedElement.offsetHeight))
|
||||
|
||||
draggedElement.style.left = newX + 'px'
|
||||
draggedElement.style.top = newY + 'px'
|
||||
@@ -834,9 +838,21 @@ function showProperties(field: FormField): void {
|
||||
propertiesPanel.innerHTML = `
|
||||
<div class="space-y-3">
|
||||
<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">
|
||||
<div id="nameError" class="hidden text-red-400 text-xs mt-1"></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}
|
||||
<div>
|
||||
<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
|
||||
const propName = document.getElementById('propName') as HTMLInputElement
|
||||
const nameError = document.getElementById('nameError') as HTMLDivElement
|
||||
const propTooltip = document.getElementById('propTooltip') as HTMLInputElement
|
||||
const propRequired = document.getElementById('propRequired') as HTMLInputElement
|
||||
const propReadOnly = document.getElementById('propReadOnly') as HTMLInputElement
|
||||
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) => {
|
||||
const newName = (e.target as HTMLInputElement).value.trim()
|
||||
|
||||
if (!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');
|
||||
if (!validateName(newName)) {
|
||||
(e.target as HTMLInputElement).value = field.name
|
||||
return
|
||||
}
|
||||
@@ -893,6 +934,27 @@ function showProperties(field: FormField): void {
|
||||
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) => {
|
||||
field.required = (e.target as HTMLInputElement).checked
|
||||
})
|
||||
@@ -1255,18 +1317,41 @@ downloadBtn.addEventListener('click', async () => {
|
||||
// Check for duplicate field names before generating PDF
|
||||
const nameCount = new Map<string, number>()
|
||||
const duplicates: string[] = []
|
||||
const conflictsWithPdf: string[] = []
|
||||
|
||||
fields.forEach(field => {
|
||||
const count = nameCount.get(field.name) || 0
|
||||
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) => {
|
||||
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) {
|
||||
const duplicateList = duplicates.map(name => `"${name}"`).join(', ')
|
||||
showModal(
|
||||
@@ -1318,14 +1403,24 @@ downloadBtn.addEventListener('click', async () => {
|
||||
const pdfPage = pdfDoc.getPage(field.pageIndex)
|
||||
const { height: pageHeight } = pdfPage.getSize()
|
||||
|
||||
const scaleX = 1 / currentScale
|
||||
const scaleY = 1 / currentScale
|
||||
const scaleX = 1 / pdfViewerScale
|
||||
const scaleY = 1 / pdfViewerScale
|
||||
|
||||
const x = field.x * scaleX
|
||||
const y = pageHeight - field.y * scaleY - field.height * scaleY // PDF coordinates from bottom
|
||||
const adjustedX = field.x - pdfViewerOffset.x
|
||||
const adjustedY = field.y - pdfViewerOffset.y
|
||||
|
||||
const x = adjustedX * scaleX
|
||||
const y = pageHeight - (adjustedY * scaleY) - (field.height * scaleY)
|
||||
const width = field.width * scaleX
|
||||
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') {
|
||||
const textField = form.createTextField(field.name)
|
||||
const rgbColor = hexToRgb(field.textColor)
|
||||
@@ -1398,14 +1493,23 @@ downloadBtn.addEventListener('click', async () => {
|
||||
}
|
||||
|
||||
} else if (field.type === 'radio') {
|
||||
const groupName = field.groupName || 'RadioGroup1'
|
||||
const groupName = field.name
|
||||
let radioGroup
|
||||
|
||||
if (radioGroups.has(groupName)) {
|
||||
radioGroup = radioGroups.get(groupName)
|
||||
} else {
|
||||
radioGroup = form.createRadioGroup(groupName)
|
||||
radioGroups.set(groupName, radioGroup)
|
||||
const existingField = form.getFieldMaybe(groupName)
|
||||
|
||||
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, {
|
||||
@@ -1585,7 +1689,7 @@ downloadBtn.addEventListener('click', async () => {
|
||||
|
||||
// Add Date Format and Keystroke Actions to the FIELD (not widget)
|
||||
const dateFormat = field.dateFormat || 'mm/dd/yyyy'
|
||||
|
||||
|
||||
const formatAction = pdfDoc.context.obj({
|
||||
Type: 'Action',
|
||||
S: 'JavaScript',
|
||||
@@ -1707,6 +1811,9 @@ downloadBtn.addEventListener('click', async () => {
|
||||
const pdfBytes = await pdfDoc.save()
|
||||
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' })
|
||||
downloadFile(blob, 'fillable-form.pdf')
|
||||
showModal('Success', 'Your PDF has been downloaded successfully.', 'info', () => {
|
||||
resetToInitial()
|
||||
}, 'Okay')
|
||||
} catch (error) {
|
||||
console.error('Error generating PDF:', error)
|
||||
const errorMessage = (error as Error).message
|
||||
@@ -1716,7 +1823,12 @@ downloadBtn.addEventListener('click', async () => {
|
||||
// Extract the field name from the error message
|
||||
const match = errorMessage.match(/A field already exists with the specified name: "(.+?)"/)
|
||||
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 {
|
||||
showModal('Error', 'Error generating PDF: ' + errorMessage, 'error')
|
||||
}
|
||||
@@ -1805,7 +1917,7 @@ function switchToPage(pageIndex: number): void {
|
||||
}
|
||||
|
||||
// Render the canvas for the current page
|
||||
function renderCanvas(): void {
|
||||
async function renderCanvas(): Promise<void> {
|
||||
const currentPage = pages[currentPageIndex]
|
||||
if (!currentPage) return
|
||||
|
||||
@@ -1814,6 +1926,7 @@ function renderCanvas(): void {
|
||||
|
||||
currentScale = scale
|
||||
|
||||
// Use actual PDF page dimensions (not scaled)
|
||||
const canvasWidth = currentPage.width * scale
|
||||
const canvasHeight = currentPage.height * scale
|
||||
|
||||
@@ -1822,20 +1935,135 @@ function renderCanvas(): void {
|
||||
|
||||
canvas.innerHTML = ''
|
||||
|
||||
if (currentPage.pdfPageData) {
|
||||
const img = document.createElement('img')
|
||||
img.src = currentPage.pdfPageData as string
|
||||
img.style.position = 'absolute'
|
||||
img.style.top = '0'
|
||||
img.style.left = '0'
|
||||
img.style.width = '100%'
|
||||
img.style.height = '100%'
|
||||
img.style.pointerEvents = 'none'
|
||||
img.style.opacity = '0.8' // Slightly transparent so fields are visible
|
||||
canvas.appendChild(img)
|
||||
if (uploadedPdfDoc) {
|
||||
try {
|
||||
const arrayBuffer = await uploadedPdfDoc.save()
|
||||
const blob = new Blob([arrayBuffer.buffer as ArrayBuffer], { type: 'application/pdf' })
|
||||
const blobUrl = URL.createObjectURL(blob)
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.src = `/pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}#page=${currentPageIndex + 1}&toolbar=0`
|
||||
iframe.style.width = '100%'
|
||||
iframe.style.height = `${canvasHeight}px`
|
||||
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 => {
|
||||
renderField(field)
|
||||
})
|
||||
@@ -1904,25 +2132,41 @@ async function handlePdfUpload(file: File) {
|
||||
uploadedPdfDoc = await PDFDocument.load(arrayBuffer)
|
||||
|
||||
// Check for existing fields and update counter
|
||||
existingFieldNames.clear()
|
||||
try {
|
||||
const form = uploadedPdfDoc.getForm()
|
||||
const fields = form.getFields()
|
||||
fields.forEach(field => {
|
||||
const pdfFields = form.getFields()
|
||||
|
||||
// console.log('📋 Found', pdfFields.length, 'existing fields in uploaded PDF')
|
||||
|
||||
pdfFields.forEach(field => {
|
||||
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+)/)
|
||||
if (match) {
|
||||
const num = parseInt(match[2])
|
||||
if (!isNaN(num) && num >= fieldCounter) {
|
||||
if (!isNaN(num) && num > fieldCounter) {
|
||||
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) {
|
||||
// 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()
|
||||
pages = []
|
||||
@@ -1931,27 +2175,11 @@ async function handlePdfUpload(file: File) {
|
||||
const page = uploadedPdfDoc.getPage(i)
|
||||
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({
|
||||
index: i,
|
||||
width,
|
||||
height,
|
||||
pdfPageData: dataUrl as any
|
||||
pdfPageData: undefined
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2005,17 +2233,26 @@ const errorModalTitle = document.getElementById('errorModalTitle')
|
||||
const errorModalMessage = document.getElementById('errorModalMessage')
|
||||
const errorModalClose = document.getElementById('errorModalClose')
|
||||
|
||||
function showModal(title: string, message: string, type: 'error' | 'warning' | 'info' = 'error') {
|
||||
if (!errorModal || !errorModalTitle || !errorModalMessage) return
|
||||
let modalCloseCallback: (() => void) | null = null
|
||||
|
||||
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
|
||||
errorModalMessage.textContent = message
|
||||
errorModalClose.textContent = buttonText
|
||||
|
||||
modalCloseCallback = onClose || null
|
||||
errorModal.classList.remove('hidden')
|
||||
}
|
||||
|
||||
if (errorModalClose) {
|
||||
errorModalClose.addEventListener('click', () => {
|
||||
errorModal?.classList.add('hidden')
|
||||
if (modalCloseCallback) {
|
||||
modalCloseCallback()
|
||||
modalCloseCallback = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2024,6 +2261,10 @@ if (errorModal) {
|
||||
errorModal.addEventListener('click', (e) => {
|
||||
if (e.target === errorModal) {
|
||||
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 { pdfToWebp } from './pdf-to-webp.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 { extractPages } from './extract-pages.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 { sanitizePdf } from './sanitize-pdf.js';
|
||||
import { removeRestrictions } from './remove-restrictions.js';
|
||||
import { repairPdf } from './repair-pdf.js';
|
||||
|
||||
export const toolLogic = {
|
||||
merge: { process: merge, setup: setupMergeTool },
|
||||
@@ -74,6 +75,7 @@ export const toolLogic = {
|
||||
encrypt,
|
||||
decrypt,
|
||||
'remove-restrictions': removeRestrictions,
|
||||
'repair-pdf': repairPdf,
|
||||
organize,
|
||||
rotate,
|
||||
'add-page-numbers': addPageNumbers,
|
||||
@@ -90,7 +92,7 @@ export const toolLogic = {
|
||||
'png-to-pdf': pngToPdf,
|
||||
'pdf-to-webp': pdfToWebp,
|
||||
'webp-to-pdf': webpToPdf,
|
||||
'delete-pages': deletePages,
|
||||
'delete-pages': { process: deletePages, setup: setupDeletePagesTool },
|
||||
'add-blank-page': addBlankPage,
|
||||
'extract-pages': extractPages,
|
||||
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
||||
|
||||
+143
-115
@@ -1,17 +1,30 @@
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.ts';
|
||||
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.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 { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
|
||||
|
||||
const mergeState = {
|
||||
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: {},
|
||||
pdfBytes: {},
|
||||
activeMode: 'file',
|
||||
sortableInstances: {},
|
||||
isRendering: false,
|
||||
@@ -19,47 +32,16 @@ const mergeState = {
|
||||
lastFileHash: null,
|
||||
};
|
||||
|
||||
function parsePageRanges(rangeString: any, totalPages: any) {
|
||||
const indices = new Set();
|
||||
if (!rangeString.trim()) return [];
|
||||
|
||||
const ranges = rangeString.split(',');
|
||||
for (const range of ranges) {
|
||||
const trimmedRange = range.trim();
|
||||
if (trimmedRange.includes('-')) {
|
||||
const [start, end] = trimmedRange.split('-').map(Number);
|
||||
if (
|
||||
isNaN(start) ||
|
||||
isNaN(end) ||
|
||||
start < 1 ||
|
||||
end > totalPages ||
|
||||
start > end
|
||||
)
|
||||
continue;
|
||||
for (let i = start; i <= end; i++) {
|
||||
indices.add(i - 1);
|
||||
}
|
||||
} else {
|
||||
const pageNum = Number(trimmedRange);
|
||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||
indices.add(pageNum - 1);
|
||||
}
|
||||
}
|
||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||
return Array.from(indices).sort((a, b) => a - b);
|
||||
}
|
||||
const mergeWorker = new Worker('/workers/merge.worker.js');
|
||||
|
||||
function initializeFileListSortable() {
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) return;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
if (mergeState.sortableInstances.fileList) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
mergeState.sortableInstances.fileList.destroy();
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
|
||||
handle: '.drag-handle',
|
||||
animation: 150,
|
||||
@@ -79,13 +61,10 @@ function initializePageThumbnailsSortable() {
|
||||
const container = document.getElementById('page-merge-preview');
|
||||
if (!container) return;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
if (mergeState.sortableInstances.pageThumbnails) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
mergeState.sortableInstances.pageThumbnails.destroy();
|
||||
}
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
|
||||
animation: 150,
|
||||
ghostClass: 'sortable-ghost',
|
||||
@@ -128,13 +107,13 @@ async function renderPageMergeThumbnails() {
|
||||
mergeState.isRendering = true;
|
||||
container.textContent = '';
|
||||
|
||||
// Cleanup any previous lazy loading observers
|
||||
cleanupLazyRendering();
|
||||
|
||||
let totalPages = state.files.reduce((sum, file) => {
|
||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
||||
}, 0);
|
||||
let totalPages = 0;
|
||||
for (const file of state.files) {
|
||||
const doc = mergeState.pdfDocs[file.name];
|
||||
if (doc) totalPages += doc.numPages;
|
||||
}
|
||||
|
||||
try {
|
||||
let currentPageNumber = 0;
|
||||
@@ -176,11 +155,8 @@ async function renderPageMergeThumbnails() {
|
||||
|
||||
// Render pages from all files progressively
|
||||
for (const file of state.files) {
|
||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||
if (!pdfDoc) continue;
|
||||
|
||||
const pdfData = await pdfDoc.save();
|
||||
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise;
|
||||
const pdfjsDoc = mergeState.pdfDocs[file.name];
|
||||
if (!pdfjsDoc) continue;
|
||||
|
||||
// Create a wrapper function that includes the file name
|
||||
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {
|
||||
@@ -193,7 +169,7 @@ async function renderPageMergeThumbnails() {
|
||||
container,
|
||||
createWrapperWithFileName,
|
||||
{
|
||||
batchSize: 6,
|
||||
batchSize: 8,
|
||||
useLazyLoading: true,
|
||||
lazyLoadMargin: '300px',
|
||||
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;
|
||||
@@ -229,91 +201,159 @@ async function renderPageMergeThumbnails() {
|
||||
export async function merge() {
|
||||
showLoader('Merging PDFs...');
|
||||
try {
|
||||
const newPdfDoc = await PDFLibDocument.create();
|
||||
const jobs: MergeJob[] = [];
|
||||
const filesToMerge: MergeFile[] = [];
|
||||
const uniqueFileNames = new Set<string>();
|
||||
|
||||
if (mergeState.activeMode === 'file') {
|
||||
const fileList = document.getElementById('file-list');
|
||||
if (!fileList) throw new Error('File list not found');
|
||||
|
||||
const sortedFiles = Array.from(fileList.children)
|
||||
.map((li) => {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
return state.files.find((f) => f.name === li.dataset.fileName);
|
||||
return state.files.find((f) => f.name === (li as HTMLElement).dataset.fileName);
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
for (const file of sortedFiles) {
|
||||
if (!file) continue;
|
||||
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
const rangeInput = document.getElementById(`range-${safeFileName}`);
|
||||
if (!rangeInput) continue;
|
||||
const rangeInput = document.getElementById(`range-${safeFileName}`) as HTMLInputElement;
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||
const rangeInputValue = rangeInput.value;
|
||||
const sourcePdf = mergeState.pdfDocs[file.name];
|
||||
if (!sourcePdf) continue;
|
||||
uniqueFileNames.add(file.name);
|
||||
|
||||
const totalPages = sourcePdf.getPageCount();
|
||||
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
|
||||
|
||||
const indicesToCopy =
|
||||
pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
||||
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||
if (rangeInput && rangeInput.value.trim()) {
|
||||
jobs.push({
|
||||
fileName: file.name,
|
||||
rangeType: 'specific',
|
||||
rangeString: rangeInput.value.trim()
|
||||
});
|
||||
} else {
|
||||
jobs.push({
|
||||
fileName: file.name,
|
||||
rangeType: 'all'
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Page Mode
|
||||
const pageContainer = document.getElementById('page-merge-preview');
|
||||
if (!pageContainer) throw new Error('Page container not found');
|
||||
const pageElements = Array.from(pageContainer.children);
|
||||
|
||||
const rawPages: { fileName: string; pageIndex: number }[] = [];
|
||||
for (const el of pageElements) {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const fileName = el.dataset.fileName;
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||
const pageIndex = parseInt(el.dataset.pageIndex, 10);
|
||||
const element = el as HTMLElement;
|
||||
const fileName = element.dataset.fileName;
|
||||
const pageIndex = parseInt(element.dataset.pageIndex || '', 10); // 0-based index from dataset
|
||||
|
||||
const sourcePdf = mergeState.pdfDocs[fileName];
|
||||
if (sourcePdf && !isNaN(pageIndex)) {
|
||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [
|
||||
pageIndex,
|
||||
]);
|
||||
newPdfDoc.addPage(copiedPage);
|
||||
if (fileName && !isNaN(pageIndex)) {
|
||||
uniqueFileNames.add(fileName);
|
||||
rawPages.push({ fileName, pageIndex });
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
downloadFile(
|
||||
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
||||
'merged.pdf'
|
||||
);
|
||||
showAlert('Success', 'PDFs merged successfully!');
|
||||
if (jobs.length === 0) {
|
||||
showAlert('Error', 'No files or pages selected to merge.');
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
console.error('Merge error:', e);
|
||||
showAlert(
|
||||
'Error',
|
||||
'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
|
||||
);
|
||||
} finally {
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupMergeTool() {
|
||||
document.getElementById('merge-options').classList.remove('hidden');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||
document.getElementById('process-btn').disabled = false;
|
||||
document.getElementById('merge-options')?.classList.remove('hidden');
|
||||
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
|
||||
if (processBtn) processBtn.disabled = false;
|
||||
|
||||
const wasInPageMode = mergeState.activeMode === 'page';
|
||||
|
||||
showLoader('Loading PDF documents...');
|
||||
try {
|
||||
mergeState.pdfDocs = {};
|
||||
mergeState.pdfBytes = {};
|
||||
|
||||
for (const file of state.files) {
|
||||
if (!mergeState.pdfDocs[file.name]) {
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(
|
||||
pdfBytes as ArrayBuffer,
|
||||
{
|
||||
ignoreEncryption: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||
mergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
|
||||
|
||||
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
|
||||
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
|
||||
mergeState.pdfDocs[file.name] = pdfjsDoc;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading PDFs:', error);
|
||||
@@ -329,10 +369,12 @@ export async function setupMergeTool() {
|
||||
const pagePanel = document.getElementById('page-mode-panel');
|
||||
const fileList = document.getElementById('file-list');
|
||||
|
||||
if (!fileModeBtn || !pageModeBtn || !filePanel || !pagePanel || !fileList) return;
|
||||
|
||||
fileList.textContent = ''; // Clear list safely
|
||||
(state.files as File[]).forEach((f) => {
|
||||
const doc = mergeState.pdfDocs[f.name];
|
||||
const pageCount = doc ? doc.getPageCount() : 'N/A';
|
||||
const pageCount = doc ? doc.numPages : 'N/A';
|
||||
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||
|
||||
const li = document.createElement('li');
|
||||
@@ -377,8 +419,8 @@ export async function setupMergeTool() {
|
||||
|
||||
initializeFileListSortable();
|
||||
|
||||
const newFileModeBtn = fileModeBtn.cloneNode(true);
|
||||
const newPageModeBtn = pageModeBtn.cloneNode(true);
|
||||
const newFileModeBtn = fileModeBtn.cloneNode(true) as HTMLElement;
|
||||
const newPageModeBtn = pageModeBtn.cloneNode(true) as HTMLElement;
|
||||
fileModeBtn.replaceWith(newFileModeBtn);
|
||||
pageModeBtn.replaceWith(newPageModeBtn);
|
||||
|
||||
@@ -389,13 +431,9 @@ export async function setupMergeTool() {
|
||||
filePanel.classList.remove('hidden');
|
||||
pagePanel.classList.add('hidden');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
});
|
||||
|
||||
@@ -406,13 +444,9 @@ export async function setupMergeTool() {
|
||||
filePanel.classList.add('hidden');
|
||||
pagePanel.classList.remove('hidden');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
|
||||
await renderPageMergeThumbnails();
|
||||
@@ -423,20 +457,14 @@ export async function setupMergeTool() {
|
||||
filePanel.classList.add('hidden');
|
||||
pagePanel.classList.remove('hidden');
|
||||
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
|
||||
await renderPageMergeThumbnails();
|
||||
} else {
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Sortable from 'sortablejs';
|
||||
import { downloadFile, getPDFDocument } from '../utils/helpers';
|
||||
import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils';
|
||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||
import { repairPdfFile } from './repair-pdf.js';
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
@@ -351,8 +352,27 @@ async function loadPdfs(files: File[]) {
|
||||
if (renderCancelled) break;
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
let arrayBuffer: 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);
|
||||
const pdfIndex = currentPdfDocs.length - 1;
|
||||
|
||||
@@ -735,7 +755,7 @@ async function handleInsertPdf(e: Event) {
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
|
||||
currentPdfDocs.push(pdfDoc);
|
||||
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++) {
|
||||
const file = pdfDocs[j];
|
||||
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 pageCount = pdfDoc.getPageCount();
|
||||
const reversedIndices = Array.from(
|
||||
|
||||
+42
-11
@@ -3,25 +3,56 @@ import { downloadFile, resetAndReloadTool } from '../utils/helpers.js';
|
||||
import { state } from '../state.js';
|
||||
import { getRotationState, resetRotationState } from '../handlers/fileHandler.js';
|
||||
|
||||
import { degrees } from 'pdf-lib';
|
||||
import { PDFDocument, degrees } from 'pdf-lib';
|
||||
|
||||
export async function rotate() {
|
||||
showLoader('Applying rotations...');
|
||||
try {
|
||||
const pages = state.pdfDoc.getPages();
|
||||
const originalPdf = state.pdfDoc;
|
||||
const pageCount = originalPdf.getPageCount();
|
||||
const rotationStateArray = getRotationState();
|
||||
|
||||
// Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated
|
||||
rotationStateArray.forEach((rotation, pageIndex) => {
|
||||
if (rotation !== 0 && pages[pageIndex]) {
|
||||
const currentRotation = pages[pageIndex].getRotation().angle;
|
||||
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
||||
}
|
||||
});
|
||||
const newPdfDoc = await PDFDocument.create();
|
||||
|
||||
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(
|
||||
new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
|
||||
new Blob([rotatedPdfBytes as any], { type: 'application/pdf' }),
|
||||
'rotated.pdf'
|
||||
);
|
||||
|
||||
|
||||
+19
-4
@@ -80,6 +80,12 @@ const init = () => {
|
||||
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
|
||||
const footer = document.querySelector('footer');
|
||||
if (footer) {
|
||||
@@ -291,17 +297,26 @@ const init = () => {
|
||||
console.log('Please share our tool and share the love!');
|
||||
|
||||
|
||||
const githubStarsElement = document.getElementById('github-stars');
|
||||
if (githubStarsElement && !__SIMPLE_MODE__) {
|
||||
const githubStarsElements = [
|
||||
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')
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
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(() => {
|
||||
githubStarsElement.textContent = '-';
|
||||
githubStarsElements.forEach(el => {
|
||||
if (el) el.textContent = '-';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+174
-40
@@ -44,19 +44,23 @@ export const dom = {
|
||||
};
|
||||
|
||||
export const showLoader = (text = 'Processing...') => {
|
||||
dom.loaderText.textContent = text;
|
||||
dom.loaderModal.classList.remove('hidden');
|
||||
if (dom.loaderText) dom.loaderText.textContent = text;
|
||||
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) => {
|
||||
dom.alertTitle.textContent = title;
|
||||
dom.alertMessage.textContent = message;
|
||||
dom.alertModal.classList.remove('hidden');
|
||||
if (dom.alertTitle) dom.alertTitle.textContent = title;
|
||||
if (dom.alertMessage) dom.alertMessage.textContent = message;
|
||||
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) => {
|
||||
if (view === 'grid') {
|
||||
@@ -125,7 +129,7 @@ function initializeOrganizeSortable(containerId: any) {
|
||||
* @param {object} pdfDoc The loaded pdf-lib document instance.
|
||||
*/
|
||||
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);
|
||||
if (!container) return;
|
||||
|
||||
@@ -134,6 +138,9 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||
// Cleanup any previous lazy loading observers
|
||||
cleanupLazyRendering();
|
||||
|
||||
const currentRenderId = Date.now();
|
||||
container.dataset.renderId = currentRenderId.toString();
|
||||
|
||||
showLoader('Rendering page previews...');
|
||||
|
||||
const pdfData = await pdfDoc.save();
|
||||
@@ -185,7 +192,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||
|
||||
wrapper.append(pageNumSpan, deleteBtn);
|
||||
} 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)
|
||||
const rotationStateArray = getRotationState();
|
||||
@@ -202,35 +209,122 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||
|
||||
wrapper.appendChild(imgContainer);
|
||||
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'flex items-center justify-center gap-3 w-full';
|
||||
|
||||
// Page Number Overlay (Top Left)
|
||||
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();
|
||||
wrapper.appendChild(pageNumSpan);
|
||||
|
||||
const rotateBtn = document.createElement('button');
|
||||
rotateBtn.className =
|
||||
'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
|
||||
rotateBtn.title = 'Rotate 90°';
|
||||
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>';
|
||||
rotateBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const card = (e.currentTarget as HTMLElement).closest(
|
||||
'.page-rotator-item'
|
||||
) as HTMLElement;
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'flex flex-col lg:flex-row items-center justify-center w-full gap-2 px-1';
|
||||
|
||||
// Custom Stepper Component
|
||||
const stepperContainer = document.createElement('div');
|
||||
stepperContainer.className = 'flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden w-24 h-8';
|
||||
|
||||
const decrementBtn = document.createElement('button');
|
||||
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';
|
||||
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 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);
|
||||
} 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;
|
||||
@@ -243,7 +337,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||
container,
|
||||
createWrapper,
|
||||
{
|
||||
batchSize: 6,
|
||||
batchSize: 8,
|
||||
useLazyLoading: true,
|
||||
lazyLoadMargin: '300px',
|
||||
onProgress: (current, total) => {
|
||||
@@ -251,12 +345,17 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
||||
},
|
||||
onBatchComplete: () => {
|
||||
createIcons({ icons });
|
||||
},
|
||||
shouldCancel: () => {
|
||||
return container.dataset.renderId !== currentRenderId.toString();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (toolId === 'organize') {
|
||||
initializeOrganizeSortable(containerId);
|
||||
} else if (toolId === 'delete-pages') {
|
||||
// No sortable needed for delete pages
|
||||
}
|
||||
|
||||
// Reinitialize lucide icons for dynamically added elements
|
||||
@@ -576,15 +675,49 @@ export const toolTemplates = {
|
||||
<div id="rotate-all-controls" class="hidden my-6">
|
||||
<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>
|
||||
<div class="flex justify-center gap-4">
|
||||
<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>
|
||||
Rotate All Left
|
||||
</button>
|
||||
<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">
|
||||
<i data-lucide="rotate-cw" class="mr-2 h-4 w-4"></i>
|
||||
Rotate All Right
|
||||
</button>
|
||||
<div class="flex flex-col md:flex-row justify-center gap-6 items-center">
|
||||
|
||||
<!-- 90 Degree 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 90 degrees</span>
|
||||
<div class="flex gap-2">
|
||||
<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°">
|
||||
<i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i>
|
||||
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>
|
||||
@@ -814,7 +947,7 @@ export const toolTemplates = {
|
||||
flatten: () => `
|
||||
<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>
|
||||
${createFileInputHTML()}
|
||||
${createFileInputHTML({ multiple: true, showControls: true })}
|
||||
<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>
|
||||
`,
|
||||
@@ -897,6 +1030,7 @@ export const toolTemplates = {
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface RenderConfig {
|
||||
batchSize?: number;
|
||||
useLazyLoading?: boolean;
|
||||
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;
|
||||
onPageRendered?: (pageIndex: number, element: HTMLElement) => void;
|
||||
onBatchComplete?: () => void;
|
||||
@@ -214,7 +214,7 @@ export async function renderPagesProgressively(
|
||||
const {
|
||||
batchSize = 8, // Increased from 5 to 8 for faster initial render
|
||||
useLazyLoading = true,
|
||||
eagerLoadBatches = 1, // Eagerly load 1 batch ahead by default
|
||||
eagerLoadBatches = 2, // Eagerly load 1 batch ahead by default
|
||||
onProgress,
|
||||
onBatchComplete,
|
||||
} = config;
|
||||
@@ -318,7 +318,7 @@ export function observePlaceholder(
|
||||
* Eagerly renders the next batch in the background
|
||||
*/
|
||||
function renderEagerBatch(config: RenderConfig): void {
|
||||
const { eagerLoadBatches = 1, batchSize = 8 } = config;
|
||||
const { eagerLoadBatches = 2, batchSize = 8 } = config;
|
||||
|
||||
if (eagerLoadBatches <= 0 || lazyLoadState.eagerLoadQueue.length === 0) {
|
||||
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 name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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" />
|
||||
</head>
|
||||
|
||||
@@ -105,7 +108,7 @@
|
||||
</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="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
@@ -139,6 +142,9 @@
|
||||
<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>
|
||||
@@ -163,7 +169,7 @@
|
||||
class="text-gray-400 hover:text-indigo-400" title="Discord">
|
||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.19 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
|
||||
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"
|
||||
@@ -187,6 +193,7 @@
|
||||
</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/add-stamps.ts"></script>
|
||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||
|
||||
+20
-10
@@ -5,7 +5,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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/bookmark.css" rel="stylesheet" />
|
||||
</head>
|
||||
@@ -271,7 +274,7 @@
|
||||
<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" />
|
||||
<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>
|
||||
<span>Add to Page <span id="current-page-display">1</span></span>
|
||||
</button>
|
||||
@@ -391,11 +394,14 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<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
|
||||
</button>
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
@@ -415,7 +421,7 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
|
||||
<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">
|
||||
@@ -430,13 +436,13 @@
|
||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
|
||||
<a href="../../about.html" class="hover:text-indigo-400">About Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
<a href="../../faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
<a href="../../contact.html" class="hover:text-indigo-400">Contact Us</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -445,10 +451,13 @@
|
||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||
<ul class="space-y-2 text-gray-400">
|
||||
<li>
|
||||
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
|
||||
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
|
||||
</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>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -492,6 +501,7 @@
|
||||
</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/bookmark-pdf.ts"></script>
|
||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||
|
||||
+27
-20
@@ -5,7 +5,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Create PDF Form - BentoPDF</title>
|
||||
<link rel="icon" type="image/png" href="../../images/favicon.svg" />
|
||||
<link 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" />
|
||||
</head>
|
||||
|
||||
@@ -298,7 +301,7 @@
|
||||
</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="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
@@ -332,6 +335,9 @@
|
||||
<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>
|
||||
@@ -378,27 +384,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Custom Modal -->
|
||||
<div id="errorModal"
|
||||
class="hidden fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-75">
|
||||
<div class="bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 border border-gray-700">
|
||||
<div class="p-6">
|
||||
<h3 id="errorModalTitle" class="text-xl font-bold text-white mb-3">Error</h3>
|
||||
<p id="errorModalMessage" class="text-gray-300 mb-6">An error occurred.</p>
|
||||
<button id="errorModalClose"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded transition-colors">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Custom Modal -->
|
||||
<div id="errorModal" class="hidden fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-75">
|
||||
<div class="bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 border border-gray-700">
|
||||
<div class="p-6">
|
||||
<h3 id="errorModalTitle" class="text-xl font-bold text-white mb-3">Error</h3>
|
||||
<p id="errorModalMessage" class="text-gray-300 mb-6">An error occurred.</p>
|
||||
<button id="errorModalClose"
|
||||
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded transition-colors">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="../js/utils/full-width.ts"></script>
|
||||
<script type="module" src="../version.ts"></script>
|
||||
<script type="module" src="../js/logic/form-creator.ts"></script>
|
||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||
<script type="module" src="../js/utils/lucide-init.ts"></script>
|
||||
<script type="module" src="../js/utils/full-width.ts"></script>
|
||||
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="../version.ts"></script>
|
||||
<script type="module" src="../js/logic/form-creator.ts"></script>
|
||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -5,7 +5,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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" />
|
||||
</head>
|
||||
|
||||
@@ -105,7 +108,7 @@
|
||||
</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="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
@@ -139,6 +142,9 @@
|
||||
<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>
|
||||
@@ -188,6 +194,7 @@
|
||||
<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/full-width.ts"></script>
|
||||
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="../version.ts"></script>
|
||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||
</body>
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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" />
|
||||
|
||||
<style>
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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" />
|
||||
</head>
|
||||
|
||||
@@ -99,7 +102,7 @@
|
||||
</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="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
@@ -133,6 +136,9 @@
|
||||
<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>
|
||||
@@ -182,6 +188,7 @@
|
||||
<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/full-width.ts"></script>
|
||||
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
|
||||
<script type="module" src="../version.ts"></script>
|
||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||
</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 name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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" />
|
||||
</head>
|
||||
|
||||
@@ -171,7 +174,7 @@
|
||||
</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="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
||||
<div class="mb-8 md:mb-0">
|
||||
@@ -182,6 +185,9 @@
|
||||
<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>
|
||||
@@ -202,6 +208,9 @@
|
||||
<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>
|
||||
@@ -250,6 +259,7 @@
|
||||
</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="../js/logic/table-of-contents.ts"></script>
|
||||
<script type="module" src="../js/mobileMenu.ts"></script>
|
||||
</body>
|
||||
|
||||
+4
-1
@@ -5,7 +5,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Terms and Conditions - Bentopdf</title>
|
||||
<link rel="icon" type="image/png" href="images/favicon.svg" />
|
||||
<link 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" />
|
||||
</head>
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ export default defineConfig(({ mode }) => ({
|
||||
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
|
||||
'add-stamps': resolve(__dirname, 'src/pages/add-stamps.html'),
|
||||
'form-creator': resolve(__dirname, 'src/pages/form-creator.html'),
|
||||
'repair-pdf': resolve(__dirname, 'src/pages/repair-pdf.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user