Compare commits

...
11 Commits
Author SHA1 Message Date
abdullahalam123 f213d9dc27 Release v1.10.0 2025-12-01 15:15:51 +05:30
abdullahalam123 b39d2c267c Merge branch 'pdf-multi-tool-bug-fixes' 2025-12-01 14:56:09 +05:30
abdullahalam123 09436a689d feat(rotate,delete-pages): Add custom rotation angles and enhance page management UI
- Add TypeScript type definitions for merge and alternate-merge Web Workers
- Implement custom rotation angle input with increment/decrement controls for rotate tool
- Extend delete-pages tool to render page thumbnails for better UX
- Hide number input spin buttons across all number inputs via CSS
- Refactor rotateAll function to accept angle parameter instead of direction multiplier
- Update fileHandler to support delete-pages tool thumbnail rendering
- Improve type safety in alternate-merge logic with proper interface definitions
- Enhance rotate tool UI with custom angle input field and adjustment buttons
2025-12-01 14:54:46 +05:30
abdullahalam123 c5764e4172 feat(favicon,repair-pdf): Add favicon support and implement PDF repair tool
- Add favicon.ico to public directory for browser tab display
- Update favicon references across all HTML pages with proper link tags for SVG, PNG, and ICO formats
- Add Apple touch icon support for iOS devices
- Create new repair-pdf.ts logic module for PDF repair functionality
- Create new repair-pdf-page.ts utility module for page-level repair operations
- Add repair-pdf.html page with UI for PDF repair tool
- Register repair PDF tool in tools configuration and PDF tools registry
- Update UI rendering utilities to support new repair tool
- Improve favicon handling with multiple format fallbacks for cross-browser compatibility
- Standardize favicon paths to use absolute URLs for consistency
- Clean up whitespace and formatting in licensing.html for code consistency
2025-12-01 12:44:34 +05:30
Alam 2741b29ad1 Merge pull request #177 from RJGhugo/patch-2
Update nginx.conf
2025-11-30 19:45:19 +05:30
abdullahalam123 05bc0d5a3c Release v1.9.0 2025-11-28 20:50:05 +05:30
abdullahalam123 8651ca366f feat(merge): Migrate merge operations to Web Workers and reorganize tool categories
- Create merge.worker.js and alternate-merge.worker.js for offloading PDF merge operations to background threads
- Migrate merge and alternate-merge logic to use Web Worker architecture for improved performance
- Move flatten tool from singlePdfLoadTools to multiFileTools configuration
- Update merge and alternate-merge tool subtitles to indicate bookmark preservation
- Refactor alternate-merge.ts to use Web Worker instead of pdf-lib for interleaving operations
- Refactor merge.ts to use Web Worker for standard merge operations
- Update fileHandler.ts to recognize flatten as a multi-file tool requiring process button handling
- Simplify form-creator.ts and flatten.ts implementations with Web Worker integration
- Update UI state management to support Web Worker-based PDF processing
- Improve performance by moving computationally intensive PDF operations off the main thread
2025-11-28 20:49:49 +05:30
abdullahalam123 6b7163991e feat(ui): Add GitHub integration and enhance hero section design
- Add GitHub star counter badge to navigation (desktop and mobile)
- Implement grid pattern background in hero section
- Redesign hero title with inline PDF Toolkit badge and briefcase icon
- Update feature badges styling with improved visual hierarchy
- Refactor CTA button layout with icon integration
- Add simple-mode-footer utility component for footer management
- Update all tool pages with consistent styling improvements
- Enhance responsive design for mobile navigation with GitHub link
- Improve visual consistency across all pages with updated color scheme
2025-11-26 18:33:08 +05:30
abdullahalam123 f3f6af079f Release v1.8.1 2025-11-25 14:52:41 +05:30
abdullahalam123 6e7563d0c2 fix(bookmark-pdf): Improve page reference resolution and simplify destination handling
- Refactor findPageIndex() to use multiple resolution strategies (direct ref comparison, object number matching, string representation, numeric values)
- Add resolvedPageRef parameter to findPageIndex() for more efficient page dictionary matching
- Improve named destination handling to properly extract 'D' entry from dictionary-wrapped destinations
- Remove unnecessary resolveRef() call on destination arrays to preserve array structure integrity
- Pass resolved page reference to findPageIndex() for better accuracy in page lookup
- Simplify footer markup across multiple pages (add-stamps, bookmark, form-creator, json-to-pdf, pdf-to-json, table-of-contents) by removing redundant background color and grid layout
- Enhance error handling and logging for page reference resolution diagnostics
2025-11-25 14:52:34 +05:30
RJGhugo 41a35586c7 Update nginx.conf
listen [::]:8080;

Missing IPv6 plus SEMICOLONE...

https://github.com/alam00000/bentopdf/issues/136
2025-11-19 10:52:58 +01:00
46 changed files with 2059 additions and 716 deletions

No files matched your search

+4 -1
View File
@@ -13,7 +13,10 @@
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
+4 -1
View File
@@ -13,7 +13,10 @@
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
+4 -1
View File
@@ -13,7 +13,10 @@
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
+114 -28
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BentoPDF - The Privacy First PDF Toolkit</title> <title>BentoPDF - The Privacy First PDF Toolkit</title>
<link rel="icon" type="image/png" href="./images/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link href="/src/css/styles.css" rel="stylesheet" /> <link href="/src/css/styles.css" rel="stylesheet" />
</head> </head>
@@ -27,10 +30,38 @@
<a href="./contact.html" class="nav-link">Contact</a> <a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a> <a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
<a href="https://github.com/alam00000/bentopdf/" target="_blank" rel="noopener noreferrer" class="
inline-flex items-center gap-1.5 text-sm font-medium
bg-gray-800 text-gray-200 border border-gray-600
pl-2.5 pr-3 py-1
rounded-full transition-colors duration-200
shadow-sm hover:shadow-md hover:bg-gray-700
">
<svg class="w-4 h-4 flex-shrink-0 text-gray-200" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
<span id="github-stars-desktop">-</span>
</a>
</div> </div>
<!-- Mobile Hamburger Button --> <!-- Mobile Hamburger Button -->
<div class="md:hidden flex items-center"> <div class="md:hidden flex items-center gap-4">
<a href="https://github.com/alam00000/bentopdf/" target="_blank" rel="noopener noreferrer" class="
inline-flex items-center gap-1.5 text-sm font-medium
bg-gray-800 text-gray-200 border border-gray-600
pl-2.5 pr-3 py-1
rounded-full transition-colors duration-200
shadow-sm hover:shadow-md hover:bg-gray-700
">
<svg class="w-4 h-4 flex-shrink-0 text-gray-200" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
<span id="github-stars-mobile">-</span>
</a>
<button id="mobile-menu-button" type="button" <button id="mobile-menu-button" type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu" aria-expanded="false"> aria-controls="mobile-menu" aria-expanded="false">
@@ -62,52 +93,106 @@
</div> </div>
</nav> </nav>
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8"> <div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
<section id="hero-section" class="text-center py-20"> <section id="hero-section" class="text-center py-20 relative">
<div class="bg-grid-pattern"></div>
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4"> <h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
The <span class="marker-slanted"> PDF Toolkit </span> built for The <span
class="inline-flex items-center gap-3 border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 align-middle">
PDF Toolkit <i data-lucide="briefcase" class="w-8 h-8 md:w-12 md:h-12 transform -rotate-4"></i> </span> built
for
privacy<span privacy<span
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span> class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
</h1> </h1>
<p class="text-lg text-gray-400 mb-8">Fast, Secure and Forever Free.</p> <!-- <p class="text-lg text-gray-400 mb-12">Fast, Secure and Forever Free.</p> -->
<div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8"> <div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-12 mt-12">
<span <span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm"> class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium">
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-4 h-4 text-gray-400"></i>
No Signups No Signups
</span> </span>
<span <span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm"> class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium">
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-4 h-4 text-gray-400"></i>
Unlimited Use Unlimited Use
</span> </span>
<span <span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm"> class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium">
<i data-lucide="check-circle" class="w-4 h-4"></i> <i data-lucide="check-circle" class="w-4 h-4 text-gray-400"></i>
Works Offline Works Offline
</span> </span>
</div> </div>
<div class="flex flex-col items-center gap-4"> <div class="flex flex-col items-center gap-4">
<a href="#tools-header" <a href="#tools-header"
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">Start class="inline-flex items-center gap-3 pl-8 pr-2 py-2 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">
Using - Forever Free</a> Start Using Now
<a href="https://github.com/alam00000/bentopdf/stargazers" target="_blank" rel="noopener noreferrer" class=" <span class="bg-white text-indigo-600 rounded-full p-2">
inline-flex items-center gap-1.5 text-sm font-medium <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
bg-white text-gray-800 border border-gray-300 stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 <path d="M5 12h14" />
pl-2.5 pr-3 py-1 <path d="m12 5 7 7-7 7" />
rounded-full transition-colors duration-200 </svg>
shadow-sm hover:shadow-md hover:bg-gray-50 dark:hover:bg-gray-700 </span>
"> </a>
<svg class="w-4 h-4 flex-shrink-0 text-gray-800 dark:text-gray-200" fill="currentColor" viewBox="0 0 24 24"
aria-hidden="true"> </div>
</section>
<!-- USED BY SECTION START -->
<section class="py-10 hide-section">
<div class="container mx-auto px-4">
<p class="text-center text-gray-500 text-sm font-medium mb-8">Used by companies and people working at</p>
<div
class="flex flex-wrap justify-center items-center gap-8 md:gap-12 opacity-70 grayscale hover:grayscale-0 transition-all duration-500">
<!-- Google -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 24 24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .533 5.347.533 12S5.867 24 12.48 24c3.44 0 6.013-1.133 8.053-3.227 2.107-2.08 2.72-5.027 2.72-7.44 0-.733-.053-1.44-.16-2.133h-10.613z" />
</svg>
<!-- Microsoft -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 24 24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M0 0h11.377v11.372H0zM12.623 0H24v11.372H12.623zM0 12.623h11.377V24H0zM12.623 12.623H24V24H12.623z" />
</svg>
<!-- Reddit -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 24 24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z" />
</svg>
<!-- Amazon -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 48 48"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M25.4026553,25.9595294 C24.660417,27.4418824 23.3876054,28.3962353 22.0103725,28.7181176 C21.8015298,28.7181176 21.4826213,28.8225882 21.1637129,28.8225882 C18.835399,28.8225882 17.458166,27.0211765 17.458166,24.3727059 C17.458166,20.9788235 19.4703937,19.392 22.0103725,18.6465882 C23.3876054,18.3303529 24.9793255,18.2230588 26.5682233,18.2230588 L26.5682233,19.4964706 C26.5682233,21.9331765 26.6726447,23.8390588 25.4026553,25.9595294 L25.4026553,25.9595294 Z M26.5682233,13.3524706 C25.1909904,13.4569412 23.5992703,13.5614118 22.0103725,13.7703529 C19.574815,14.0922353 17.1392576,14.5157647 15.1298521,15.4701176 C11.2098182,17.0597647 8.55977364,20.4508235 8.55977364,25.4287059 C8.55977364,31.6856471 12.5842289,34.8621176 17.6726531,34.8621176 C19.3659723,34.8621176 20.7432053,34.6475294 22.0103725,34.3341176 C24.0282445,33.696 25.7187415,32.5298824 27.7309692,30.4094118 C28.8965372,31.9990588 29.2182679,32.7444706 31.2276733,34.4385882 C31.7582467,34.6475294 32.28882,34.6475294 32.7093276,34.3341176 C33.9821392,33.2724706 36.208854,31.3637647 37.3715998,30.3049412 C37.9021732,29.8814118 37.7977518,29.2432941 37.4760212,28.7181176 C36.3132753,27.2329412 35.1448851,25.9595294 35.1448851,23.0992941 L35.1448851,13.5614118 C35.1448851,9.53505882 35.4666157,5.82494118 32.5004849,3.072 C30.0649275,0.849882353 26.2493149,0 23.2831841,0 L22.0103725,0 C16.6115064,0.313411765 10.8937319,2.64564706 9.61809814,9.32329412 C9.40643324,10.1731765 10.0442501,10.4894118 10.4675799,10.5938824 L16.3998415,11.3364706 C17.0348362,11.2291765 17.3537447,10.6983529 17.458166,10.1731765 C17.9859172,7.84094118 19.8937235,6.67482353 22.0103725,6.46023529 L22.4365245,6.46023529 C23.7093361,6.46023529 25.086569,6.99105882 25.8259851,8.05270588 C26.6726447,9.32329412 26.5682233,11.0202353 26.5682233,12.5054118 L26.5682233,13.3524706 L26.5682233,13.3524706 Z">
</path>
<path
d="M47.9943556,35.9463529 L47.9943556,35.9435294 C47.971778,35.4437647 47.8673567,35.0625882 47.658514,34.7463529 L47.6359364,34.7152941 L47.6105366,34.6842353 C47.3988717,34.4527059 47.1956734,34.3651765 46.9755419,34.2691765 C46.3179696,34.0150588 45.3612442,33.8795294 44.2097872,33.8767059 C43.382883,33.8767059 42.4713128,33.9557647 41.5540982,34.1562353 L41.551276,34.0941176 L40.6284171,34.4018824 L40.6114839,34.4103529 L40.0893771,34.5797647 L40.0893771,34.6023529 C39.47696,34.8564706 38.9209869,35.1727059 38.4045245,35.5482353 C38.0827939,35.7882353 37.8175072,36.1072941 37.8033962,36.5957647 C37.7949296,36.8611765 37.9303952,37.1661176 38.1533489,37.3468235 C38.3763025,37.5275294 38.6359448,37.5896471 38.8645429,37.5896471 C38.9181647,37.5896471 38.9689643,37.5868235 39.0141194,37.5783529 L39.0592746,37.5755294 L39.093141,37.5698824 C39.5446928,37.4738824 40.2022651,37.4089412 40.9727253,37.3016471 C41.6331198,37.2282353 42.3330251,37.1745882 42.9397978,37.1745882 C43.368772,37.1717647 43.7554132,37.2028235 44.0206999,37.2592941 C44.1533432,37.2875294 44.2521202,37.3214118 44.3057419,37.3496471 C44.3254973,37.3552941 44.3396083,37.3637647 44.3480749,37.3694118 C44.3593637,37.4061176 44.3762969,37.5021176 44.3734747,37.6348235 C44.3791191,38.1430588 44.164632,39.0861176 43.8683012,40.0065882 C43.5804369,40.9270588 43.2304843,41.8503529 42.999064,42.4630588 C42.94262,42.6042353 42.9059314,42.7595294 42.9059314,42.9289412 C42.900287,43.1745882 43.0018862,43.4738824 43.2163733,43.6715294 C43.425216,43.8691765 43.696147,43.9482353 43.9219229,43.9482353 L43.9332117,43.9482353 C44.2718756,43.9454118 44.5597398,43.8098824 44.8080933,43.6150588 C47.1505182,41.5087059 47.9661336,38.1430588 48,36.2484706 L47.9943556,35.9463529 Z M41.0489247,38.8658824 C40.8090378,38.8630588 40.5635065,38.9195294 40.3349084,39.0268235 C40.0780883,39.1284706 39.8156239,39.2470588 39.5672704,39.3515294 L39.2032068,39.504 L38.7290774,39.6931765 L38.7290774,39.6988235 C33.5785648,41.7882353 28.16841,43.0136471 23.1618295,43.1209412 C22.9783866,43.1265882 22.7921215,43.1265882 22.614323,43.1265882 C14.7403887,43.1322353 8.31706456,39.4785882 1.83729642,35.8785882 C1.61152053,35.76 1.37727804,35.6978824 1.15150215,35.6978824 C0.860815683,35.6978824 0.561662624,35.808 0.344353327,36.0112941 C0.12704403,36.2174118 -0.00277710907,36.5138824 4.50895989e-05,36.816 C-0.00277710907,37.2084706 0.208887791,37.5698824 0.505218651,37.8042353 C6.58705678,43.0870588 13.25309,47.9943529 22.2192152,48 C22.3941915,48 22.57199,47.9943529 22.7497885,47.9915294 C28.453452,47.8644706 34.902176,45.936 39.9087564,42.7905882 L39.9398006,42.7708235 C40.5945507,42.3783529 41.2493008,41.9322353 41.8673623,41.4381176 C42.2511813,41.1529412 42.516468,40.7068235 42.516468,40.2437647 C42.4995348,39.4221176 41.8024517,38.8658824 41.0489247,38.8658824 Z">
</path>
</svg>
<!-- Oracle -->
<svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors" viewBox="0 0 16 16"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" <path fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" d="M.1 8c0 2.761 2.237 5 4.997 5h5.806A4.999 4.999 0 0015.9 8c0-2.761-2.237-5-4.997-5H5.097A4.999 4.999 0 00.1 8zm13.911 0a3.235 3.235 0 01-3.234 3.237h-5.55A3.235 3.235 0 011.991 8a3.235 3.235 0 013.234-3.236h5.551A3.235 3.235 0 0114.011 8z"
clip-rule="evenodd" /> clip-rule="evenodd" />
</svg> </svg>
<span id="github-stars">-</span> <!-- Apple -->
</a> <svg class="h-6 md:h-8 w-auto text-gray-400 hover:text-white transition-colors -ml-3" viewBox="0 0 24 24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path
d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.78 1.18-.19 2.31-.89 3.51-.84 1.54.06 2.77.74 3.69 1.74-3.54 1.84-2.86 6.53.51 7.91-.48 1.13-1.1 2.25-2.79 3.38zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
</svg>
</div>
</div> </div>
</section> </section>
@@ -192,7 +277,8 @@
class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500" class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Search for a tool (e.g., 'split', 'organize'...)" /> placeholder="Search for a tool (e.g., 'split', 'organize'...)" />
<span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2 gap-2"> <span class="absolute inset-y-0 right-0 flex items-center rounded-lg pr-2 gap-2">
<kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd> <kbd id="shortcut"
class="bg-gray-800 px-2.5 py-1.5 rounded-md text-xs font-semibold text-gray-300 border border-gray-700 shadow-sm"></kbd>
<button id="open-shortcuts-btn" <button id="open-shortcuts-btn"
class="p-1 hover:bg-gray-600 rounded-md text-gray-400 hover:text-white transition-colors hidden-on-touch" class="p-1 hover:bg-gray-600 rounded-md text-gray-400 hover:text-white transition-colors hidden-on-touch"
title="Keyboard Shortcuts"> title="Keyboard Shortcuts">
+15 -5
View File
@@ -13,7 +13,10 @@
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
@@ -192,7 +195,8 @@
<div <div
class="mt-8 bg-gradient-to-r from-indigo-900/40 to-purple-900/40 rounded-lg p-6 border border-indigo-500/50"> class="mt-8 bg-gradient-to-r from-indigo-900/40 to-purple-900/40 rounded-lg p-6 border border-indigo-500/50">
<div class="text-center"> <div class="text-center">
<div class="inline-block bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full mb-3 animate-pulse"> <div
class="inline-block bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full mb-3 animate-pulse">
LIMITED TIME OFFER LIMITED TIME OFFER
</div> </div>
<p class="text-white text-2xl font-bold mb-2">Lifetime License</p> <p class="text-white text-2xl font-bold mb-2">Lifetime License</p>
@@ -202,7 +206,7 @@
<span class="text-lg font-normal text-gray-400">one-time</span> <span class="text-lg font-normal text-gray-400">one-time</span>
</div> </div>
<p class="text-gray-300 text-sm mb-4">Includes all feature updates forever</p> <p class="text-gray-300 text-sm mb-4">Includes all feature updates forever</p>
<!-- Unlimited Usage Info --> <!-- Unlimited Usage Info -->
<div class="bg-indigo-900/30 rounded-lg p-4 mb-4 border border-indigo-500/30"> <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"> <div class="flex items-start gap-3 text-left">
@@ -210,12 +214,18 @@
<div> <div>
<h4 class="text-white font-semibold text-sm mb-2">Unlimited Usage</h4> <h4 class="text-white font-semibold text-sm mb-2">Unlimited Usage</h4>
<p class="text-gray-300 text-xs leading-relaxed"> <p class="text-gray-300 text-xs leading-relaxed">
The BentoPDF commercial license permits use on an <strong class="text-white">unrestricted, unlimited number of devices, servers, and user machines</strong> within your organization. There are <strong class="text-white">no per-user, per-machine, or per-seat limitations</strong>. Once you purchase the license, all your internal users may use BentoPDF through your browser-based system without requiring additional licenses. The BentoPDF commercial license permits use on an <strong
class="text-white">unrestricted, unlimited number of devices, servers,
and user machines</strong> within your organization. There are <strong
class="text-white">no per-user, per-machine, or per-seat
limitations</strong>. Once you purchase the license, all your internal
users may use BentoPDF through your browser-based system without requiring
additional licenses.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<a href="https://ko-fi.com/s/f32ca4cb75" target="_blank" rel="noopener noreferrer" <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"> 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 Purchase Lifetime License
+2 -1
View File
@@ -18,6 +18,7 @@ http {
server { server {
listen 8080; listen 8080;
listen [::]:8080;
server_name localhost; server_name localhost;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
@@ -37,4 +38,4 @@ http {
add_header Cross-Origin-Opener-Policy "same-origin" always; add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "cross-origin" always; add_header Cross-Origin-Resource-Policy "cross-origin" always;
} }
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"private": true, "private": true,
"version": "1.8.0", "version": "1.10.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
+4 -1
View File
@@ -13,7 +13,10 @@
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo"> <div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<span class="text-white font-bold text-xl ml-2"> <span class="text-white font-bold text-xl ml-2">
<a href="index.html">BentoPDF</a> <a href="index.html">BentoPDF</a>
</span> </span>
Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

+3 -1
View File
@@ -1,5 +1,7 @@
<svg width="86" height="166" viewBox="0 0 86 166" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="166" height="166" viewBox="0 0 166 166" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(40, 0)">
<path d="M56.7848 80.9116L0 25.6211L24.8434 1.43151C26.8037 -0.47717 29.9814 -0.47717 31.9414 1.43151L85.177 53.2662L56.7848 80.9116Z" fill="#A5B4FC"/> <path d="M56.7848 80.9116L0 25.6211L24.8434 1.43151C26.8037 -0.47717 29.9814 -0.47717 31.9414 1.43151L85.177 53.2662L56.7848 80.9116Z" fill="#A5B4FC"/>
<path d="M-1.30805e-05 80.7335L21.2939 101.467L42.5878 80.7335L21.2939 60L-1.30805e-05 80.7335Z" fill="#E0E7FF"/> <path d="M-1.30805e-05 80.7335L21.2939 101.467L42.5878 80.7335L21.2939 60L-1.30805e-05 80.7335Z" fill="#E0E7FF"/>
<path d="M56.7848 83L81.6279 107.19C83.5881 109.098 83.5881 112.192 81.6279 114.101L28.3925 165.936L0 138.29L56.7848 83Z" fill="#6366F1"/> <path d="M56.7848 83L81.6279 107.19C83.5881 109.098 83.5881 112.192 81.6279 114.101L28.3925 165.936L0 138.29L56.7848 83Z" fill="#6366F1"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 510 B

After

Width:  |  Height:  |  Size: 550 B

+23
View File
@@ -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;
+63
View File
@@ -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'
});
}
}
+33
View File
@@ -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;
+70
View File
@@ -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
View File
@@ -257,9 +257,11 @@ footer a {
z-index: -1; z-index: -1;
} }
.section-divider { .section-divider {
height: 1px; height: 1px;
background: linear-gradient(to right, transparent, #4f46e5, transparent); background: linear-gradient(to right, transparent, #4d44f7, transparent);
/* Fades from transparent to indigo and back */ /* Fades from transparent to indigo and back */
margin: 2rem auto; margin: 2rem auto;
/* my-16 */ /* my-16 */
@@ -289,9 +291,6 @@ footer a {
box-shadow: box-shadow:
0 10px 15px -3px rgba(79, 70, 229, 0.3), 0 10px 15px -3px rgba(79, 70, 229, 0.3),
0 4px 6px -4px rgba(79, 70, 229, 0.3); 0 4px 6px -4px rgba(79, 70, 229, 0.3);
/* hover:shadow-xl hover:shadow-indigo-500/30 */
transform: translateY(-0.25rem);
/* hover:-translate-y-1 */
} }
.btn-gradient:focus { .btn-gradient:focus {
@@ -588,4 +587,29 @@ button:disabled,
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
visibility: visible; visibility: visible;
}
.bg-grid-pattern {
position: absolute;
inset: 0;
z-index: -1;
background-size: 60px 60px;
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
-webkit-mask-image: radial-gradient(ellipse at center, black 40%, transparent 80%);
pointer-events: none;
}
/* Hide spin buttons for number inputs */
input[type="number"] {
appearance: none;
-moz-appearance: textfield; /* Firefox */
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none; /* Chrome, Safari, Edge */
margin: 0;
} }
+3 -1
View File
@@ -9,7 +9,7 @@ export const singlePdfLoadTools = [
'pdf-to-greyscale', 'pdf-to-greyscale',
'edit-metadata', 'edit-metadata',
'remove-metadata', 'remove-metadata',
'flatten',
'delete-pages', 'delete-pages',
'add-blank-page', 'add-blank-page',
'extract-pages', 'extract-pages',
@@ -68,4 +68,6 @@ export const multiFileTools = [
'txt-to-pdf', 'txt-to-pdf',
'compress', 'compress',
'extract-attachments', 'extract-attachments',
'flatten',
'repair-pdf',
]; ];
+8 -2
View File
@@ -13,7 +13,7 @@ export const categories = [
id: 'merge', id: 'merge',
name: 'Merge PDF', name: 'Merge PDF',
icon: 'combine', icon: 'combine',
subtitle: 'Combine multiple PDFs into one file.', subtitle: 'Combine multiple PDFs into one file. Preserves Bookmarks.',
}, },
{ {
id: 'split', id: 'split',
@@ -310,7 +310,7 @@ export const categories = [
id: 'alternate-merge', id: 'alternate-merge',
name: 'Alternate & Mix Pages', name: 'Alternate & Mix Pages',
icon: 'shuffle', icon: 'shuffle',
subtitle: 'Combine PDFs by alternating pages from each.', subtitle: 'Merge PDFs by alternating pages from each PDF. Preserves Bookmarks',
}, },
{ {
id: 'organize', id: 'organize',
@@ -468,6 +468,12 @@ export const categories = [
subtitle: subtitle:
'Remove password protection and security restrictions associated with digitally signed PDF files.', 'Remove password protection and security restrictions associated with digitally signed PDF files.',
}, },
{
href: '/src/pages/repair-pdf.html',
name: 'Repair PDF',
icon: 'wrench',
subtitle: 'Recover data from corrupted or damaged PDF files.',
},
], ],
}, },
{ {
+40 -6
View File
@@ -117,7 +117,7 @@ async function handleSinglePdfUpload(toolId, file) {
.toString(); .toString();
} }
if (toolId === 'organize' || toolId === 'rotate') { if (toolId === 'organize' || toolId === 'rotate' || toolId === 'delete-pages') {
await renderPageThumbnails(toolId, state.pdfDoc); await renderPageThumbnails(toolId, state.pdfDoc);
if (toolId === 'rotate') { if (toolId === 'rotate') {
@@ -134,14 +134,19 @@ async function handleSinglePdfUpload(toolId, file) {
const rotateAllRightBtn = document.getElementById( const rotateAllRightBtn = document.getElementById(
'rotate-all-right-btn' 'rotate-all-right-btn'
); );
const rotateAllCustomBtn = document.getElementById('rotate-all-custom-btn');
const rotateAllCustomInput = document.getElementById('custom-rotate-all-input') as HTMLInputElement;
const rotateAllDecrementBtn = document.getElementById('rotate-all-decrement-btn');
const rotateAllIncrementBtn = document.getElementById('rotate-all-increment-btn');
rotateAllControls.classList.remove('hidden'); rotateAllControls.classList.remove('hidden');
createIcons({ icons }); createIcons({ icons });
const rotateAll = (direction) => { const rotateAll = (angle: number) => {
// Update rotation state for ALL pages (including unrendered ones) // Update rotation state for ALL pages (including unrendered ones)
for (let i = 0; i < rotationState.length; i++) { for (let i = 0; i < rotationState.length; i++) {
rotationState[i] = (rotationState[i] + direction * 90 + 360) % 360; rotationState[i] = (rotationState[i] + angle);
} }
// Update DOM for currently rendered pages // Update DOM for currently rendered pages
@@ -149,15 +154,44 @@ async function handleSinglePdfUpload(toolId, file) {
const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0'); const pageIndex = parseInt((item as HTMLElement).dataset.pageIndex || '0');
const newRotation = rotationState[pageIndex]; const newRotation = rotationState[pageIndex];
(item as HTMLElement).dataset.rotation = newRotation.toString(); (item as HTMLElement).dataset.rotation = newRotation.toString();
const thumbnail = item.querySelector('canvas, img'); const thumbnail = item.querySelector('canvas, img');
if (thumbnail) { if (thumbnail) {
(thumbnail as HTMLElement).style.transform = (thumbnail as HTMLElement).style.transform =
`rotate(${newRotation}deg)`; `rotate(${newRotation}deg)`;
} }
const input = item.querySelector('input');
if (input) {
input.value = newRotation.toString();
}
}); });
}; };
rotateAllLeftBtn.onclick = () => rotateAll(-1); rotateAllLeftBtn.onclick = () => rotateAll(-90);
rotateAllRightBtn.onclick = () => rotateAll(1); rotateAllRightBtn.onclick = () => rotateAll(90);
if (rotateAllCustomBtn && rotateAllCustomInput) {
rotateAllCustomBtn.onclick = () => {
const angle = parseInt(rotateAllCustomInput.value);
if (!isNaN(angle) && angle !== 0) {
rotateAll(angle);
}
};
if (rotateAllDecrementBtn) {
rotateAllDecrementBtn.onclick = () => {
let current = parseInt(rotateAllCustomInput.value) || 0;
rotateAllCustomInput.value = (current - 1).toString();
};
}
if (rotateAllIncrementBtn) {
rotateAllIncrementBtn.onclick = () => {
let current = parseInt(rotateAllCustomInput.value) || 0;
rotateAllCustomInput.value = (current + 1).toString();
};
}
}
} }
} }
@@ -754,7 +788,7 @@ export function setupFileInputHandler(toolId) {
} }
if (isMultiFileTool) { if (isMultiFileTool) {
if (toolId === 'txt-to-pdf' || toolId === 'compress' || toolId === 'extract-attachments') { if (toolId === 'txt-to-pdf' || toolId === 'compress' || toolId === 'extract-attachments' || toolId === 'flatten') {
const processBtn = document.getElementById('process-btn'); const processBtn = document.getElementById('process-btn');
if (processBtn) { if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false; (processBtn as HTMLButtonElement).disabled = false;
+57 -32
View File
@@ -1,13 +1,20 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument } from 'pdf-lib';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
const alternateMergeState = { interface AlternateMergeState {
pdfDocs: Record<string, any>;
pdfBytes: Record<string, ArrayBuffer>;
}
const alternateMergeState: AlternateMergeState = {
pdfDocs: {}, pdfDocs: {},
pdfBytes: {},
}; };
const alternateMergeWorker = new Worker('/workers/alternate-merge.worker.js');
export async function setupAlternateMergeTool() { export async function setupAlternateMergeTool() {
const optionsDiv = document.getElementById('alternate-merge-options'); const optionsDiv = document.getElementById('alternate-merge-options');
const processBtn = document.getElementById( const processBtn = document.getElementById(
@@ -23,18 +30,18 @@ export async function setupAlternateMergeTool() {
fileList.innerHTML = ''; fileList.innerHTML = '';
alternateMergeState.pdfDocs = {}; alternateMergeState.pdfDocs = {};
alternateMergeState.pdfBytes = {};
showLoader('Loading PDF documents...'); showLoader('Loading PDF documents...');
try { try {
for (const file of state.files) { for (const file of state.files) {
const pdfBytes = await readFileAsArrayBuffer(file); const pdfBytes = await readFileAsArrayBuffer(file);
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load( alternateMergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
pdfBytes as ArrayBuffer,
{ const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
ignoreEncryption: true, const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
} alternateMergeState.pdfDocs[file.name] = pdfjsDoc;
); const pageCount = pdfjsDoc.numPages;
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
const li = document.createElement('li'); const li = document.createElement('li');
li.className = li.className =
@@ -79,7 +86,7 @@ export async function setupAlternateMergeTool() {
} }
export async function alternateMerge() { export async function alternateMerge() {
if (Object.keys(alternateMergeState.pdfDocs).length < 2) { if (Object.keys(alternateMergeState.pdfBytes).length < 2) {
showAlert( showAlert(
'Not Enough Files', 'Not Enough Files',
'Please upload at least two PDF files to alternate and mix.' 'Please upload at least two PDF files to alternate and mix.'
@@ -89,37 +96,55 @@ export async function alternateMerge() {
showLoader('Alternating and mixing pages...'); showLoader('Alternating and mixing pages...');
try { try {
const newPdfDoc = await PDFDocument.create();
const fileList = document.getElementById('alternate-file-list'); const fileList = document.getElementById('alternate-file-list');
if (!fileList) throw new Error('File list not found');
const sortedFileNames = Array.from(fileList.children).map( const sortedFileNames = Array.from(fileList.children).map(
(li) => (li as HTMLElement).dataset.fileName (li) => (li as HTMLElement).dataset.fileName
); ).filter(Boolean) as string[];
const loadedDocs = sortedFileNames.map( const filesToMerge: InterleaveFile[] = [];
(name) => alternateMergeState.pdfDocs[name] for (const name of sortedFileNames) {
); const bytes = alternateMergeState.pdfBytes[name];
const pageCounts = loadedDocs.map((doc) => doc.getPageCount()); if (bytes) {
const maxPages = Math.max(...pageCounts); filesToMerge.push({ name, data: bytes });
for (let i = 0; i < maxPages; i++) {
for (const doc of loadedDocs) {
if (i < doc.getPageCount()) {
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
newPdfDoc.addPage(copiedPage);
}
} }
} }
const mergedPdfBytes = await newPdfDoc.save(); if (filesToMerge.length < 2) {
downloadFile( showAlert('Error', 'At least two valid PDFs are required.');
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), hideLoader();
'alternated-mixed.pdf' return;
); }
showAlert('Success', 'PDFs have been mixed successfully!');
const message: InterleaveMessage = {
command: 'interleave',
files: filesToMerge
};
alternateMergeWorker.postMessage(message, filesToMerge.map(f => f.data));
alternateMergeWorker.onmessage = (e: MessageEvent<InterleaveResponse>) => {
hideLoader();
if (e.data.status === 'success') {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
downloadFile(blob, 'alternated-mixed.pdf');
showAlert('Success', 'PDFs have been mixed successfully!');
} else {
console.error('Worker interleave error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
}
};
alternateMergeWorker.onerror = (e) => {
hideLoader();
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) { } catch (e) {
console.error('Alternate Merge error:', e); console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.'); showAlert('Error', 'An error occurred while mixing the PDFs.');
} finally {
hideLoader(); hideLoader();
} }
} }
+188 -322
View File
@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
// TODO: @ALAM - remove ts-nocheck and fix types later, possibly convert this into an npm package // TODO: @ALAM - remove ts-nocheck and fix types later, possibly convert this into an npm package
import { PDFDocument, PDFName, PDFString, PDFNumber, PDFArray } from 'pdf-lib'; import { PDFDocument, PDFName, PDFString, PDFNumber, PDFArray, PDFHexString } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
@@ -37,19 +37,19 @@ function showInputModal(title, fields = [], defaultValues = {}) {
.map((field) => { .map((field) => {
if (field.type === 'text') { if (field.type === 'text') {
return ` return `
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label> <label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
<input type="text" id="modal-${field.name}" value="${escapeHTML(defaultValues[field.name] || '')}" <input type="text" id="modal-${field.name}" value="${escapeHTML(defaultValues[field.name] || '')}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg" class="w-full px-3 py-2 border border-gray-300 rounded-lg"
placeholder="${field.placeholder || ''}" /> placeholder="${field.placeholder || ''}" />
</div> </div>
`; `;
} else if (field.type === 'select') { } else if (field.type === 'select') {
return ` return `
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label> <label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
<select id="modal-${field.name}" class="w-full px-3 py-2 border border-gray-300 rounded-lg"> <select id="modal-${field.name}" class="w-full px-3 py-2 border border-gray-300 rounded-lg">
${field.options ${field.options
.map( .map(
(opt) => ` (opt) => `
<option value="${opt.value}" ${defaultValues[field.name] === opt.value ? 'selected' : ''}> <option value="${opt.value}" ${defaultValues[field.name] === opt.value ? 'selected' : ''}>
@@ -57,91 +57,92 @@ function showInputModal(title, fields = [], defaultValues = {}) {
</option> </option>
` `
) )
.join('')} .join('')
</select> }
</select>
${field.name === 'color' ? '<input type="color" id="modal-color-picker" class="hidden w-full h-10 mt-2 rounded cursor-pointer border border-gray-300" value="#000000" />' : ''} ${field.name === 'color' ? '<input type="color" id="modal-color-picker" class="hidden w-full h-10 mt-2 rounded cursor-pointer border border-gray-300" value="#000000" />' : ''}
</div> </div>
`; `;
} else if (field.type === 'destination') { } else if (field.type === 'destination') {
const hasDestination = const hasDestination =
defaultValues.destX !== null && defaultValues.destX !== undefined; defaultValues.destX !== null && defaultValues.destX !== undefined;
return ` return `
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label> <label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
<div class="p-3 bg-gray-50 rounded-lg border border-gray-200 space-y-2"> <div class="p-3 bg-gray-50 rounded-lg border border-gray-200 space-y-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label class="flex items-center gap-1 text-xs"> <label class="flex items-center gap-1 text-xs">
<input type="checkbox" id="modal-use-destination" class="w-4 h-4" ${hasDestination ? 'checked' : ''}> <input type="checkbox" id="modal-use-destination" class="w-4 h-4" ${hasDestination ? 'checked' : ''}>
<span>Set custom destination</span> <span>Set custom destination </span>
</label> </label>
</div> </div>
<div id="destination-controls" class="${hasDestination ? '' : 'hidden'} space-y-2"> <div id="destination-controls" class="${hasDestination ? '' : 'hidden'} space-y-2">
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div> <div>
<label class="text-xs text-gray-600">Page</label> <label class="text-xs text-gray-600">Page</label>
<input type="number" id="modal-dest-page" min="1" max="${field.maxPages || 1}" value="${defaultValues.destPage || field.page || 1}" <input type="number" id="modal-dest-page" min="1" max="${field.maxPages || 1}" value="${defaultValues.destPage || field.page || 1}"
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" step="1" /> class="w-full px-2 py-1 border border-gray-300 rounded text-sm" step="1" />
</div> </div>
<div> <div>
<label class="text-xs text-gray-600">Zoom (%)</label> <label class="text-xs text-gray-600">Zoom(%)</label>
<select id="modal-dest-zoom" class="w-full px-2 py-1 border border-gray-300 rounded text-sm"> <select id="modal-dest-zoom" class="w-full px-2 py-1 border border-gray-300 rounded text-sm">
<option value="">Inherit</option> <option value="">Inherit</option>
<option value="0">Fit Page</option> <option value="0">Fit Page</option>
<option value="50">50%</option> <option value="50">50%</option>
<option value="75">75%</option> <option value="75">75%</option>
<option value="100">100%</option> <option value="100">100%</option>
<option value="125">125%</option> <option value="125">125%</option>
<option value="150">150%</option> <option value="150">150%</option>
<option value="200">200%</option> <option value="200">200%</option>
</select> </select>
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<div> <div>
<label class="text-xs text-gray-600">X Position</label> <label class="text-xs text-gray-600">X Position</label>
<input type="number" id="modal-dest-x" value="0" step="10" <input type="number" id="modal-dest-x" value="0" step="10"
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" /> class="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
</div> </div>
<div> <div>
<label class="text-xs text-gray-600">Y Position</label> <label class="text-xs text-gray-600">Y Position</label>
<input type="number" id="modal-dest-y" value="0" step="10" <input type="number" id="modal-dest-y" value="0" step="10"
class="w-full px-2 py-1 border border-gray-300 rounded text-sm" /> class="w-full px-2 py-1 border border-gray-300 rounded text-sm" />
</div> </div>
</div> </div>
<button id="modal-pick-destination" class="w-full px-3 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded text-xs flex items-center justify-center gap-1"> <button id="modal-pick-destination" class="w-full px-3 py-2 btn-gradient text-white rounded text-xs !flex items-center justify-center gap-1">
<i data-lucide="crosshair" class="w-3 h-3"></i> Click on PDF to Pick Location <i data-lucide="crosshair" class="w-3 h-3"></i> Click on PDF to Pick Location
</button> </button>
<p class="text-xs text-gray-500 italic">Click the button above, then click on the PDF where you want the bookmark to jump to</p> <p class="text-xs text-gray-500 italic">Click the button above, then click on the PDF where you want the bookmark to jump to</p>
</div> </div>
</div> </div>
</div> </div>
`; `;
} else if (field.type === 'preview') { } else if (field.type === 'preview') {
return ` return `
<div class="mb-4"> <div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label> <label class="block text-sm font-medium text-gray-700 mb-2">${field.label}</label>
<div id="modal-preview" class="style-preview bg-gray-50"> <div id="modal-preview" class="style-preview bg-gray-50">
<span id="preview-text" style="font-size: 16px;">Preview Text</span> <span id="preview-text" style="font-size: 16px;">Preview Text</span>
</div> </div>
</div> </div>
`; `;
} }
return ''; return '';
}) })
.join(''); .join('');
modal.innerHTML = ` modal.innerHTML = `
<div class="p-6"> <div class="p-6">
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3> <h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
<div class="mb-6"> <div class="mb-6">
${fieldsHTML} ${fieldsHTML}
</div> </div>
<div class="flex gap-2 justify-end"> <div class="flex gap-2 justify-end">
<button id="modal-cancel" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300">Cancel</button> <button id="modal-cancel" class="px-4 py-2 rounded-lg bg-gray-200 hover:bg-gray-300">Cancel</button>
<button id="modal-confirm" class="px-4 py-2 rounded bg-blue-500 hover:bg-blue-600 text-white">Confirm</button> <button id="modal-confirm" class="px-4 py-2 rounded btn-gradient text-white">Confirm</button>
</div> </div>
</div> </div>
`; `;
overlay.appendChild(modal); overlay.appendChild(modal);
modalContainer.appendChild(overlay); modalContainer.appendChild(overlay);
@@ -476,7 +477,7 @@ document.addEventListener('DOMContentLoaded', () => {
canvasWrapper.appendChild(coordTooltip); canvasWrapper.appendChild(coordTooltip);
} }
coordTooltip.textContent = `X: ${Math.round(x)}, Y: ${Math.round(y)}`; coordTooltip.textContent = `X: ${Math.round(x)}, Y: ${Math.round(y)} `;
coordTooltip.style.left = e.clientX - rect.left + 15 + 'px'; coordTooltip.style.left = e.clientX - rect.left + 15 + 'px';
coordTooltip.style.top = e.clientY - rect.top + 15 + 'px'; coordTooltip.style.top = e.clientY - rect.top + 15 + 'px';
}); });
@@ -525,12 +526,12 @@ document.addEventListener('DOMContentLoaded', () => {
destinationMarker = document.createElement('div'); destinationMarker = document.createElement('div');
destinationMarker.className = 'destination-marker'; destinationMarker.className = 'destination-marker';
destinationMarker.innerHTML = ` destinationMarker.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2"> <svg viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2">
<circle cx="12" cy="12" r="10" fill="#3b82f6" fill-opacity="0.2"/> <circle cx="12" cy="12" r="10" fill="#3b82f6" fill-opacity="0.2" />
<path d="M12 2 L12 22 M2 12 L22 12"/> <path d="M12 2 L12 22 M2 12 L22 12" />
<circle cx="12" cy="12" r="2" fill="#3b82f6"/> <circle cx="12" cy="12" r="2" fill="#3b82f6" />
</svg> </svg>
`; `;
const canvasRect = canvas.getBoundingClientRect(); const canvasRect = canvas.getBoundingClientRect();
const wrapperRect = canvasWrapper.getBoundingClientRect(); const wrapperRect = canvasWrapper.getBoundingClientRect();
destinationMarker.style.position = 'absolute'; destinationMarker.style.position = 'absolute';
@@ -549,7 +550,7 @@ document.addEventListener('DOMContentLoaded', () => {
canvasX + canvasRect.left - wrapperRect.left + 20 + 'px'; canvasX + canvasRect.left - wrapperRect.left + 20 + 'px';
coordDisplay.style.top = coordDisplay.style.top =
canvasY + canvasRect.top - wrapperRect.top - 30 + 'px'; canvasY + canvasRect.top - wrapperRect.top - 30 + 'px';
coordDisplay.textContent = `X: ${Math.round(pdfX)}, Y: ${Math.round(pdfY)}`; coordDisplay.textContent = `X: ${Math.round(pdfX)}, Y: ${Math.round(pdfY)} `;
canvasWrapper.appendChild(coordDisplay); canvasWrapper.appendChild(coordDisplay);
// Call callback with PDF coordinates // Call callback with PDF coordinates
@@ -577,14 +578,14 @@ function showConfirmModal(message) {
modal.className = 'modal-content'; modal.className = 'modal-content';
modal.innerHTML = ` modal.innerHTML = `
<div class="p-6"> <div class="p-6">
<h3 class="text-xl font-bold text-gray-800 mb-4">Confirm Action</h3> <h3 class="text-xl font-bold text-gray-800 mb-4">Confirm Action</h3>
<p class="text-gray-600 mb-6">${message}</p> <p class="text-gray-600 mb-6">${message}</p>
<div class="flex gap-2 justify-end"> <div class="flex gap-2 justify-end">
<button id="modal-cancel" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300">Cancel</button> <button id="modal-cancel" class="px-4 py-2 rounded-lg bg-gray-200 hover:bg-gray-300">Cancel</button>
<button id="modal-confirm" class="px-4 py-2 rounded bg-blue-500 hover:bg-blue-600 text-white">Confirm</button> <button id="modal-confirm" class="px-4 py-2 rounded btn-gradient text-white">Confirm</button>
</div> </div>
</div> </div>
`; `;
overlay.appendChild(modal); overlay.appendChild(modal);
@@ -618,14 +619,14 @@ function showAlertModal(title, message) {
modal.className = 'modal-content'; modal.className = 'modal-content';
modal.innerHTML = ` modal.innerHTML = `
<div class="p-6"> <div class="p-6">
<h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3> <h3 class="text-xl font-bold text-gray-800 mb-4">${title}</h3>
<p class="text-gray-600 mb-6">${message}</p> <p class="text-gray-600 mb-6">${message}</p>
<div class="flex justify-end"> <div class="flex justify-end">
<button id="modal-ok" class="px-4 py-2 rounded bg-blue-500 hover:bg-blue-600 text-white">OK</button> <button id="modal-ok" class="px-4 py-2 rounded btn-gradient text-white">OK</button>
</div> </div>
</div> </div>
`; `;
overlay.appendChild(modal); overlay.appendChild(modal);
modalContainer.appendChild(overlay); modalContainer.appendChild(overlay);
@@ -832,7 +833,7 @@ deleteAllBtn.addEventListener('click', async () => {
} }
const confirmed = await showConfirmModal( const confirmed = await showConfirmModal(
`Delete all ${bookmarkTree.length} bookmark(s)?` `Delete all ${bookmarkTree.length} bookmark(s) ? `
); );
if (confirmed) { if (confirmed) {
bookmarkTree = []; bookmarkTree = [];
@@ -949,7 +950,7 @@ batchDeleteBtn.addEventListener('click', async () => {
if (selectedBookmarks.size === 0) return; if (selectedBookmarks.size === 0) return;
const confirmed = await showConfirmModal( const confirmed = await showConfirmModal(
`Delete ${selectedBookmarks.size} bookmark(s)?` `Delete ${selectedBookmarks.size} bookmark(s) ? `
); );
if (!confirmed) return; if (!confirmed) return;
@@ -1173,7 +1174,7 @@ async function renderPage(num, zoom = null, destX = null, destY = null) {
ctx.stroke(); ctx.stroke();
// Draw coordinate text background // Draw coordinate text background
const text = `X: ${Math.round(destX)}, Y: ${Math.round(destY)}`; const text = `X: ${Math.round(destX)}, Y: ${Math.round(destY)} `;
ctx.font = 'bold 12px monospace'; ctx.font = 'bold 12px monospace';
const textMetrics = ctx.measureText(text); const textMetrics = ctx.measureText(text);
const textWidth = textMetrics.width; const textWidth = textMetrics.width;
@@ -1532,7 +1533,7 @@ function createNodeElement(node, level = 0) {
if (result && result.title) { if (result && result.title) {
node.children.push({ node.children.push({
id: Date.now() + Math.random(), id: Date.now() + Math.random(),
title: result.title, title: cleanTitle(result.title),
page: currentPage, page: currentPage,
children: [], children: [],
color: null, color: null,
@@ -1608,7 +1609,7 @@ function createNodeElement(node, level = 0) {
); );
if (result) { if (result) {
node.title = result.title; node.title = cleanTitle(result.title);
node.color = result.color || null; node.color = result.color || null;
node.style = result.style || null; node.style = result.style || null;
@@ -1756,7 +1757,7 @@ function parseCSV(text) {
const [, title, page, level] = match; const [, title, page, level] = match;
const bookmark = { const bookmark = {
id: Date.now() + Math.random(), id: Date.now() + Math.random(),
title: title.replace(/""/g, '"'), title: cleanTitle(title.replace(/""/g, '"')),
page: parseInt(page), page: parseInt(page),
children: [], children: [],
color: null, color: null,
@@ -1787,6 +1788,15 @@ jsonImportHidden.addEventListener('change', async (e) => {
const text = await file.text(); const text = await file.text();
try { try {
const imported = JSON.parse(text); const imported = JSON.parse(text);
// Recursively clean titles in imported JSON
function cleanImportedTree(nodes) {
if (!nodes) return;
for (const node of nodes) {
if (node.title) node.title = cleanTitle(node.title);
if (node.children) cleanImportedTree(node.children);
}
}
cleanImportedTree(imported);
bookmarkTree = imported; bookmarkTree = imported;
saveState(); saveState();
renderBookmarkTree(); renderBookmarkTree();
@@ -1834,215 +1844,84 @@ extractExistingBtn.addEventListener('click', async () => {
} }
}); });
// function cleanTitle(title) {
// // @TODO@ALAM: visit this for encoding issues later
// if (typeof title === 'string') {
// if (title.includes('€') && !title.includes(' ')) {
// return title.replace(/€/g, ' ');
// }
// return title.replace(/[\x00-\x1F\x7F-\x9F]/g, '').trim();
// }
// return title;
// }
function cleanTitle(title) {
// @TODO@ALAM: check for other encoding issues
if (typeof title === 'string') {
return title.replace(/[\x00-\x1F\x7F-\x9F]/g, '').trim();
}
return title;
}
async function extractExistingBookmarks(doc) { async function extractExistingBookmarks(doc) {
try { try {
const outlines = doc.catalog.lookup(PDFName.of('Outlines')); const outline = await pdfJsDoc.getOutline();
if (!outlines) return []; console.log(outline);
if (!outline) return [];
const pages = doc.getPages();
// Helper to resolve references
function resolveRef(obj) {
if (!obj) return null;
if (obj.lookup) return obj;
if (obj.objectNumber !== undefined && doc.context) {
return doc.context.lookup(obj);
}
return obj;
}
// Build named destinations map (support full name-tree with Kids and Catalog-level Dests)
const namedDests = new Map();
try {
function addNamePair(nameObj, destObj) {
try {
const key = nameObj.decodeText
? nameObj.decodeText()
: String(nameObj);
namedDests.set(key, resolveRef(destObj));
} catch (_) {
// ignore malformed entry
}
}
function traverseNamesNode(node) {
if (!node) return;
node = resolveRef(node);
if (!node) return;
const namesArray = node.lookup
? node.lookup(PDFName.of('Names'))
: null;
if (namesArray && namesArray.array) {
for (let i = 0; i < namesArray.array.length; i += 2) {
const n = namesArray.array[i];
const d = namesArray.array[i + 1];
addNamePair(n, d);
}
}
const kidsArray = node.lookup ? node.lookup(PDFName.of('Kids')) : null;
if (kidsArray && kidsArray.array) {
for (const kid of kidsArray.array) traverseNamesNode(kid);
}
}
// Names tree under Catalog/Names/Dests
const names = doc.catalog.lookup(PDFName.of('Names'));
if (names) {
const destsTree = names.lookup(PDFName.of('Dests'));
if (destsTree) traverseNamesNode(destsTree);
}
// Some PDFs store Dests directly under the Catalog's Dests as a dictionary
const catalogDests = doc.catalog.lookup(PDFName.of('Dests'));
if (catalogDests && catalogDests.dict) {
const entries = catalogDests.dict.entries();
for (const [key, value] of entries) {
// keys are PDFName; convert to string
const keyStr = key.decodeText ? key.decodeText() : key.toString();
namedDests.set(keyStr, resolveRef(value));
}
}
} catch (e) {
console.error('Error building named destinations:', e);
}
function findPageIndex(pageRef) {
if (!pageRef) return 0;
try {
const resolved = resolveRef(pageRef);
if (pageRef.numberValue !== undefined) {
const numericIndex = pageRef.numberValue | 0;
if (numericIndex >= 0 && numericIndex < pages.length)
return numericIndex;
}
if (pageRef.objectNumber !== undefined) {
const idxByObjNum = pages.findIndex(
(p) => p.ref.objectNumber === pageRef.objectNumber
);
if (idxByObjNum !== -1) return idxByObjNum;
}
if (pageRef.toString) {
const target = pageRef.toString();
const idxByString = pages.findIndex(
(p) => p.ref.toString() === target
);
if (idxByString !== -1) return idxByString;
}
if (resolved && resolved.get) {
for (let i = 0; i < pages.length; i++) {
const pageDict = doc.context.lookup(pages[i].ref);
if (pageDict === resolved) return i;
}
}
} catch (e) {
console.error('Error finding page:', e);
}
// Fallback: keep current behavior but log for diagnostics
console.warn('Falling back to page 0 for destination');
return 0;
}
function getDestination(item) {
if (!item) return null;
// Try Dest entry first
let dest = item.lookup(PDFName.of('Dest'));
// If no Dest, try Action/D
if (!dest) {
const action = resolveRef(item.lookup(PDFName.of('A')));
if (action) {
dest = action.lookup(PDFName.of('D'));
}
}
// Handle named destinations
if (dest && !dest.array) {
try {
const name = dest.decodeText ? dest.decodeText() : dest.toString();
if (namedDests.has(name)) {
dest = namedDests.get(name);
} else if (dest.lookup) {
// Some named destinations resolve to a dictionary with 'D' entry
const maybeDict = resolveRef(dest);
const dictD =
maybeDict && maybeDict.lookup
? maybeDict.lookup(PDFName.of('D'))
: null;
if (dictD) dest = resolveRef(dictD);
}
} catch (_) {
// leave dest as-is if it can't be decoded
}
}
return resolveRef(dest);
}
function traverse(item) {
if (!item) return null;
item = resolveRef(item);
if (!item) return null;
const title = item.lookup(PDFName.of('Title'));
const dest = getDestination(item);
const colorObj = item.lookup(PDFName.of('C'));
const flagsObj = item.lookup(PDFName.of('F'));
async function processOutlineItem(item) {
let pageIndex = 0; let pageIndex = 0;
let destX = null; let destX = null;
let destY = null; let destY = null;
let zoom = null; let zoom = null;
if (dest && dest.array) { try {
const pageRef = dest.array[0]; let dest = item.dest;
pageIndex = findPageIndex(pageRef); if (typeof dest === 'string') {
dest = await pdfJsDoc.getDestination(dest);
}
if (dest.array.length > 2) { if (Array.isArray(dest)) {
const xObj = resolveRef(dest.array[2]); const destRef = dest[0];
const yObj = resolveRef(dest.array[3]); pageIndex = await pdfJsDoc.getPageIndex(destRef);
const zoomObj = resolveRef(dest.array[4]);
if (xObj && xObj.numberValue !== undefined) destX = xObj.numberValue; if (dest.length > 2) {
if (yObj && yObj.numberValue !== undefined) destY = yObj.numberValue; const x = dest[2];
if (zoomObj && zoomObj.numberValue !== undefined) { const y = dest[3];
zoom = String(Math.round(zoomObj.numberValue * 100)); const z = dest[4];
if (typeof x === 'number') destX = x;
if (typeof y === 'number') destY = y;
if (typeof z === 'number') zoom = String(Math.round(z * 100));
} }
} }
} catch (e) {
console.warn('Error resolving destination:', e);
} }
// Rest of the color and style processing remains the same
let color = null; let color = null;
if (colorObj && colorObj.array) { if (item.color) {
const [r, g, b] = colorObj.array; const [r, g, b] = item.color;
if (r > 0.8 && g < 0.3 && b < 0.3) color = 'red'; const rN = r / 255;
else if (r < 0.3 && g < 0.3 && b > 0.8) color = 'blue'; const gN = g / 255;
else if (r < 0.3 && g > 0.8 && b < 0.3) color = 'green'; const bN = b / 255;
else if (r > 0.8 && g > 0.8 && b < 0.3) color = 'yellow';
else if (r > 0.5 && g < 0.5 && b > 0.5) color = 'purple'; if (rN > 0.8 && gN < 0.3 && bN < 0.3) color = 'red';
else if (rN < 0.3 && gN < 0.3 && bN > 0.8) color = 'blue';
else if (rN < 0.3 && gN > 0.8 && bN < 0.3) color = 'green';
else if (rN > 0.8 && gN > 0.8 && bN < 0.3) color = 'yellow';
else if (rN > 0.5 && gN < 0.5 && bN > 0.5) color = 'purple';
} }
// Map style
let style = null; let style = null;
if (flagsObj) { if (item.bold && item.italic) style = 'bold-italic';
const flags = flagsObj.numberValue || 0; else if (item.bold) style = 'bold';
const isBold = (flags & 2) !== 0; else if (item.italic) style = 'italic';
const isItalic = (flags & 1) !== 0;
if (isBold && isItalic) style = 'bold-italic';
else if (isBold) style = 'bold';
else if (isItalic) style = 'italic';
}
const bookmark = { const bookmark = {
id: Date.now() + Math.random(), id: Date.now() + Math.random(),
title: title ? title.decodeText() : 'Untitled', title: cleanTitle(item.title),
page: pageIndex + 1, page: pageIndex + 1,
children: [], children: [],
color, color,
@@ -2052,21 +1931,10 @@ async function extractExistingBookmarks(doc) {
zoom, zoom,
}; };
// Process children (make sure to resolve refs) if (item.items && item.items.length > 0) {
let child = resolveRef(item.lookup(PDFName.of('First'))); for (const childItem of item.items) {
while (child) { const childBookmark = await processOutlineItem(childItem);
const childBookmark = traverse(child); bookmark.children.push(childBookmark);
if (childBookmark) bookmark.children.push(childBookmark);
child = resolveRef(child.lookup(PDFName.of('Next')));
}
if (pageIndex === 0 && bookmark.children.length > 0) {
const firstChild = bookmark.children[0];
if (firstChild) {
bookmark.page = firstChild.page;
bookmark.destX = firstChild.destX;
bookmark.destY = firstChild.destY;
bookmark.zoom = firstChild.zoom;
} }
} }
@@ -2074,11 +1942,9 @@ async function extractExistingBookmarks(doc) {
} }
const result = []; const result = [];
let first = resolveRef(outlines.lookup(PDFName.of('First'))); for (const item of outline) {
while (first) { const bookmark = await processOutlineItem(item);
const bookmark = traverse(first); result.push(bookmark);
if (bookmark) result.push(bookmark);
first = resolveRef(first.lookup(PDFName.of('Next')));
} }
return result; return result;
@@ -2114,7 +1980,7 @@ downloadBtn.addEventListener('click', async () => {
const itemDict = pdfLibDoc.context.obj({}); const itemDict = pdfLibDoc.context.obj({});
const itemRef = pdfLibDoc.context.register(itemDict); const itemRef = pdfLibDoc.context.register(itemDict);
itemDict.set(PDFName.of('Title'), PDFString.of(node.title)); itemDict.set(PDFName.of('Title'), PDFHexString.fromText(node.title));
itemDict.set(PDFName.of('Parent'), parentRef); itemDict.set(PDFName.of('Parent'), parentRef);
// Always map bookmark page to zero-based index consistently // Always map bookmark page to zero-based index consistently
+4 -2
View File
@@ -180,7 +180,8 @@ async function performFlatteningCrop(cropData: any) {
// Load the original PDF with pdf-lib to copy un-cropped pages from // Load the original PDF with pdf-lib to copy un-cropped pages from
const sourcePdfDocForCopying = await PDFLibDocument.load( const sourcePdfDocForCopying = await PDFLibDocument.load(
cropperState.originalPdfBytes cropperState.originalPdfBytes,
{ignoreEncryption: true, throwOnInvalidObject: false}
); );
const totalPages = cropperState.pdfDoc.numPages; const totalPages = cropperState.pdfDoc.numPages;
@@ -321,7 +322,8 @@ export async function setupCropperTool() {
finalPdfBytes = await newPdfDoc.save(); finalPdfBytes = await newPdfDoc.save();
} else { } else {
const pdfToModify = await PDFLibDocument.load( const pdfToModify = await PDFLibDocument.load(
cropperState.originalPdfBytes cropperState.originalPdfBytes,
{ignoreEncryption: true, throwOnInvalidObject: false}
); );
await performMetadataCrop(pdfToModify, finalCropData); await performMetadataCrop(pdfToModify, finalCropData);
finalPdfBytes = await pdfToModify.save(); finalPdfBytes = await pdfToModify.save();
+41
View File
@@ -68,3 +68,44 @@ export async function deletePages() {
hideLoader(); hideLoader();
} }
} }
export function setupDeletePagesTool() {
const input = document.getElementById('pages-to-delete') as HTMLInputElement;
if (!input) return;
const updateHighlights = () => {
const val = input.value;
const pagesToDelete = new Set<number>();
const parts = val.split(',');
for (const part of parts) {
const trimmed = part.trim();
if (trimmed.includes('-')) {
const [start, end] = trimmed.split('-').map(Number);
if (!isNaN(start) && !isNaN(end) && start <= end) {
for (let i = start; i <= end; i++) pagesToDelete.add(i);
}
} else {
const num = Number(trimmed);
if (!isNaN(num)) pagesToDelete.add(num);
}
}
const thumbnails = document.querySelectorAll('#delete-pages-preview .page-thumbnail');
thumbnails.forEach((thumb) => {
const pageNum = parseInt((thumb as HTMLElement).dataset.pageNumber || '0');
const innerContainer = thumb.querySelector('div.relative');
if (pagesToDelete.has(pageNum)) {
innerContainer?.classList.add('border-red-500');
innerContainer?.classList.remove('border-gray-600');
} else {
innerContainer?.classList.remove('border-red-500');
innerContainer?.classList.add('border-gray-600');
}
});
};
input.addEventListener('input', updateHighlights);
updateHighlights();
}
+69 -20
View File
@@ -1,6 +1,7 @@
import { showLoader, hideLoader, showAlert } from '../ui.js'; import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js'; import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument } from 'pdf-lib';
export function flattenFormsInDoc(pdfDoc) { export function flattenFormsInDoc(pdfDoc) {
const form = pdfDoc.getForm(); const form = pdfDoc.getForm();
@@ -8,30 +9,78 @@ export function flattenFormsInDoc(pdfDoc) {
} }
export async function flatten() { export async function flatten() {
if (!state.pdfDoc) { if (state.files.length === 0) {
showAlert('Error', 'PDF not loaded.'); showAlert('No Files', 'Please select at least one PDF file.');
return; return;
} }
showLoader('Flattening PDF...');
try {
flattenFormsInDoc(state.pdfDoc);
const flattenedBytes = await state.pdfDoc.save(); try {
downloadFile( if (state.files.length === 1) {
new Blob([flattenedBytes], { type: 'application/pdf' }), showLoader('Flattening PDF...');
'flattened.pdf' const file = state.files[0];
); const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
try {
flattenFormsInDoc(pdfDoc);
} catch (e) {
if (e.message.includes('getForm')) {
// Ignore if no form found
} else {
throw e;
}
}
const flattenedBytes = await pdfDoc.save();
downloadFile(
new Blob([flattenedBytes as any], { type: 'application/pdf' }),
`flattened_${file.name}`
);
hideLoader();
} else {
showLoader('Flattening multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
let processedCount = 0;
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Flattening ${i + 1}/${state.files.length}: ${file.name}...`);
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
try {
flattenFormsInDoc(pdfDoc);
} catch (e) {
if (e.message.includes('getForm')) {
// Ignore if no form found
} else {
throw e;
}
}
const flattenedBytes = await pdfDoc.save();
zip.file(`flattened_${file.name}`, flattenedBytes);
processedCount++;
} catch (e) {
console.error(`Error processing ${file.name}:`, e);
}
}
if (processedCount > 0) {
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'flattened_pdfs.zip');
showAlert('Success', `Processed ${processedCount} PDFs.`);
} else {
showAlert('Error', 'No PDFs could be processed.');
}
hideLoader();
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
if (e.message.includes('getForm')) {
showAlert(
'No Form Found',
'This PDF does not contain any form fields to flatten.'
);
} else {
showAlert('Error', 'Could not flatten the PDF.');
}
} finally {
hideLoader(); hideLoader();
showAlert('Error', e.message || 'An unexpected error occurred.');
} }
} }
+308 -67
View File
@@ -1,8 +1,9 @@
import { PDFDocument, StandardFonts, rgb, TextAlignment, PDFName, PDFString, PageSizes, PDFBool, PDFDict, PDFArray } from 'pdf-lib' import { PDFDocument, StandardFonts, rgb, TextAlignment, PDFName, PDFString, PageSizes, PDFBool, PDFDict, PDFArray, PDFRadioGroup } from 'pdf-lib'
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js' import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js' import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js'
import { createIcons, icons } from 'lucide' import { createIcons, icons } from 'lucide'
import * as pdfjsLib from 'pdfjs-dist' import * as pdfjsLib from 'pdfjs-dist'
import 'pdfjs-dist/web/pdf_viewer.css'
// Initialize PDF.js worker // Initialize PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString() pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString()
@@ -49,18 +50,21 @@ interface PageData {
let fields: FormField[] = [] let fields: FormField[] = []
let selectedField: FormField | null = null let selectedField: FormField | null = null
let fieldCounter = 0 let fieldCounter = 0
let existingFieldNames: Set<string> = new Set()
let existingRadioGroups: Set<string> = new Set()
let draggedElement: HTMLElement | null = null let draggedElement: HTMLElement | null = null
let offsetX = 0 let offsetX = 0
let offsetY = 0 let offsetY = 0
// Multi-page state
let pages: PageData[] = [] let pages: PageData[] = []
let currentPageIndex = 0 let currentPageIndex = 0
let uploadedPdfDoc: PDFDocument | null = null let uploadedPdfDoc: PDFDocument | null = null
let uploadedPdfjsDoc: any = null
let pageSize: { width: number; height: number } = { width: 612, height: 792 } let pageSize: { width: number; height: number } = { width: 612, height: 792 }
let currentScale = 1.333 let currentScale = 1.333
let pdfViewerOffset = { x: 0, y: 0 }
let pdfViewerScale = 1.333
// Resize state
let resizing = false let resizing = false
let resizeField: FormField | null = null let resizeField: FormField | null = null
let resizePos: string | null = null let resizePos: string | null = null
@@ -419,8 +423,8 @@ function renderField(field: FormField): void {
let newX = touch.clientX - rect.left - offsetX let newX = touch.clientX - rect.left - offsetX
let newY = touch.clientY - rect.top - offsetY let newY = touch.clientY - rect.top - offsetY
newX = Math.max(0, Math.min(newX, 816 - fieldWrapper.offsetWidth)) newX = Math.max(0, Math.min(newX, rect.width - fieldWrapper.offsetWidth))
newY = Math.max(0, Math.min(newY, 1056 - fieldWrapper.offsetHeight)) newY = Math.max(0, Math.min(newY, rect.height - fieldWrapper.offsetHeight))
fieldWrapper.style.left = newX + 'px' fieldWrapper.style.left = newX + 'px'
fieldWrapper.style.top = newY + 'px' fieldWrapper.style.top = newY + 'px'
@@ -496,8 +500,8 @@ document.addEventListener('mousemove', (e) => {
let newX = e.clientX - rect.left - offsetX let newX = e.clientX - rect.left - offsetX
let newY = e.clientY - rect.top - offsetY let newY = e.clientY - rect.top - offsetY
newX = Math.max(0, Math.min(newX, 816 - draggedElement.offsetWidth)) newX = Math.max(0, Math.min(newX, rect.width - draggedElement.offsetWidth))
newY = Math.max(0, Math.min(newY, 1056 - draggedElement.offsetHeight)) newY = Math.max(0, Math.min(newY, rect.height - draggedElement.offsetHeight))
draggedElement.style.left = newX + 'px' draggedElement.style.left = newX + 'px'
draggedElement.style.top = newY + 'px' draggedElement.style.top = newY + 'px'
@@ -834,9 +838,21 @@ function showProperties(field: FormField): void {
propertiesPanel.innerHTML = ` propertiesPanel.innerHTML = `
<div class="space-y-3"> <div class="space-y-3">
<div> <div>
<label class="block text-xs font-semibold text-gray-300 mb-1">Field Name</label> <label class="block text-xs font-semibold text-gray-300 mb-1">Field Name ${field.type === 'radio' ? '(Group Name)' : ''}</label>
<input type="text" id="propName" value="${field.name}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500"> <input type="text" id="propName" value="${field.name}" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
<div id="nameError" class="hidden text-red-400 text-xs mt-1"></div>
</div> </div>
${field.type === 'radio' && (existingRadioGroups.size > 0 || fields.some(f => f.type === 'radio' && f.id !== field.id)) ? `
<div>
<label class="block text-xs font-semibold text-gray-300 mb-1">Existing Radio Groups</label>
<select id="existingGroups" class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
<option value="">-- Select existing group --</option>
${Array.from(existingRadioGroups).map(name => `<option value="${name}">${name}</option>`).join('')}
${Array.from(new Set(fields.filter(f => f.type === 'radio' && f.id !== field.id).map(f => f.name))).map(name => !existingRadioGroups.has(name) ? `<option value="${name}">${name}</option>` : '').join('')}
</select>
<p class="text-xs text-gray-400 mt-1">Select to add this button to an existing group</p>
</div>
` : ''}
${specificProps} ${specificProps}
<div> <div>
<label class="block text-xs font-semibold text-gray-300 mb-1">Tooltip / Help Text</label> <label class="block text-xs font-semibold text-gray-300 mb-1">Tooltip / Help Text</label>
@@ -858,25 +874,50 @@ function showProperties(field: FormField): void {
// Common listeners // Common listeners
const propName = document.getElementById('propName') as HTMLInputElement const propName = document.getElementById('propName') as HTMLInputElement
const nameError = document.getElementById('nameError') as HTMLDivElement
const propTooltip = document.getElementById('propTooltip') as HTMLInputElement const propTooltip = document.getElementById('propTooltip') as HTMLInputElement
const propRequired = document.getElementById('propRequired') as HTMLInputElement const propRequired = document.getElementById('propRequired') as HTMLInputElement
const propReadOnly = document.getElementById('propReadOnly') as HTMLInputElement const propReadOnly = document.getElementById('propReadOnly') as HTMLInputElement
const deleteBtn = document.getElementById('deleteBtn') as HTMLButtonElement const deleteBtn = document.getElementById('deleteBtn') as HTMLButtonElement
const validateName = (newName: string): boolean => {
if (!newName) {
nameError.textContent = 'Field name cannot be empty'
nameError.classList.remove('hidden')
propName.classList.add('border-red-500')
return false
}
if (field.type === 'radio') {
nameError.classList.add('hidden')
propName.classList.remove('border-red-500')
return true
}
const isDuplicateInFields = fields.some(f => f.id !== field.id && f.name === newName)
const isDuplicateInPdf = existingFieldNames.has(newName)
if (isDuplicateInFields || isDuplicateInPdf) {
nameError.textContent = `Field name "${newName}" already exists in this ${isDuplicateInPdf ? 'PDF' : 'form'}. Please try using a unique name.`
nameError.classList.remove('hidden')
propName.classList.add('border-red-500')
return false
}
nameError.classList.add('hidden')
propName.classList.remove('border-red-500')
return true
}
propName.addEventListener('input', (e) => {
const newName = (e.target as HTMLInputElement).value.trim()
validateName(newName)
})
propName.addEventListener('change', (e) => { propName.addEventListener('change', (e) => {
const newName = (e.target as HTMLInputElement).value.trim() const newName = (e.target as HTMLInputElement).value.trim()
if (!newName) { if (!validateName(newName)) {
showModal('Invalid Name', 'Field name cannot be empty.', 'warning');
(e.target as HTMLInputElement).value = field.name
return
}
// Check for duplicate name
const isDuplicate = fields.some(f => f.id !== field.id && f.name === newName)
if (isDuplicate) {
showModal('Duplicate Name', `A field with the name "${newName}" already exists. Please choose a unique name.`, 'error');
(e.target as HTMLInputElement).value = field.name (e.target as HTMLInputElement).value = field.name
return return
} }
@@ -893,6 +934,27 @@ function showProperties(field: FormField): void {
field.tooltip = (e.target as HTMLInputElement).value field.tooltip = (e.target as HTMLInputElement).value
}) })
if (field.type === 'radio') {
const existingGroupsSelect = document.getElementById('existingGroups') as HTMLSelectElement
if (existingGroupsSelect) {
existingGroupsSelect.addEventListener('change', (e) => {
const selectedGroup = (e.target as HTMLSelectElement).value
if (selectedGroup) {
propName.value = selectedGroup
field.name = selectedGroup
validateName(selectedGroup)
// Update field label
const fieldWrapper = document.getElementById(field.id)
if (fieldWrapper) {
const label = fieldWrapper.querySelector('.field-label') as HTMLElement
if (label) label.textContent = field.name
}
}
})
}
}
propRequired.addEventListener('change', (e) => { propRequired.addEventListener('change', (e) => {
field.required = (e.target as HTMLInputElement).checked field.required = (e.target as HTMLInputElement).checked
}) })
@@ -1255,18 +1317,41 @@ downloadBtn.addEventListener('click', async () => {
// Check for duplicate field names before generating PDF // Check for duplicate field names before generating PDF
const nameCount = new Map<string, number>() const nameCount = new Map<string, number>()
const duplicates: string[] = [] const duplicates: string[] = []
const conflictsWithPdf: string[] = []
fields.forEach(field => { fields.forEach(field => {
const count = nameCount.get(field.name) || 0 const count = nameCount.get(field.name) || 0
nameCount.set(field.name, count + 1) nameCount.set(field.name, count + 1)
if (existingFieldNames.has(field.name)) {
if (field.type === 'radio' && existingRadioGroups.has(field.name)) {
} else {
conflictsWithPdf.push(field.name)
}
}
}) })
nameCount.forEach((count, name) => { nameCount.forEach((count, name) => {
if (count > 1) { if (count > 1) {
duplicates.push(name) const fieldsWithName = fields.filter(f => f.name === name)
const allRadio = fieldsWithName.every(f => f.type === 'radio')
if (!allRadio) {
duplicates.push(name)
}
} }
}) })
if (conflictsWithPdf.length > 0) {
const conflictList = [...new Set(conflictsWithPdf)].map(name => `"${name}"`).join(', ')
showModal(
'Field Name Conflict',
`The following field names already exist in the uploaded PDF: ${conflictList}. Please rename these fields before downloading.`,
'error'
)
return
}
if (duplicates.length > 0) { if (duplicates.length > 0) {
const duplicateList = duplicates.map(name => `"${name}"`).join(', ') const duplicateList = duplicates.map(name => `"${name}"`).join(', ')
showModal( showModal(
@@ -1318,14 +1403,24 @@ downloadBtn.addEventListener('click', async () => {
const pdfPage = pdfDoc.getPage(field.pageIndex) const pdfPage = pdfDoc.getPage(field.pageIndex)
const { height: pageHeight } = pdfPage.getSize() const { height: pageHeight } = pdfPage.getSize()
const scaleX = 1 / currentScale const scaleX = 1 / pdfViewerScale
const scaleY = 1 / currentScale const scaleY = 1 / pdfViewerScale
const x = field.x * scaleX const adjustedX = field.x - pdfViewerOffset.x
const y = pageHeight - field.y * scaleY - field.height * scaleY // PDF coordinates from bottom const adjustedY = field.y - pdfViewerOffset.y
const x = adjustedX * scaleX
const y = pageHeight - (adjustedY * scaleY) - (field.height * scaleY)
const width = field.width * scaleX const width = field.width * scaleX
const height = field.height * scaleY const height = field.height * scaleY
console.log(`Field "${field.name}":`, {
screenPos: { x: field.x, y: field.y },
adjustedPos: { x: adjustedX, y: adjustedY },
pdfPos: { x, y, width, height },
metrics: { offset: pdfViewerOffset, scale: pdfViewerScale }
})
if (field.type === 'text') { if (field.type === 'text') {
const textField = form.createTextField(field.name) const textField = form.createTextField(field.name)
const rgbColor = hexToRgb(field.textColor) const rgbColor = hexToRgb(field.textColor)
@@ -1398,14 +1493,23 @@ downloadBtn.addEventListener('click', async () => {
} }
} else if (field.type === 'radio') { } else if (field.type === 'radio') {
const groupName = field.groupName || 'RadioGroup1' const groupName = field.name
let radioGroup let radioGroup
if (radioGroups.has(groupName)) { if (radioGroups.has(groupName)) {
radioGroup = radioGroups.get(groupName) radioGroup = radioGroups.get(groupName)
} else { } else {
radioGroup = form.createRadioGroup(groupName) const existingField = form.getFieldMaybe(groupName)
radioGroups.set(groupName, radioGroup)
if (existingField) {
radioGroup = existingField
radioGroups.set(groupName, radioGroup)
console.log(`Using existing radio group from PDF: ${groupName}`)
} else {
radioGroup = form.createRadioGroup(groupName)
radioGroups.set(groupName, radioGroup)
console.log(`Created new radio group: ${groupName}`)
}
} }
radioGroup.addOptionToPage(field.exportValue || 'Yes', pdfPage as any, { radioGroup.addOptionToPage(field.exportValue || 'Yes', pdfPage as any, {
@@ -1585,7 +1689,7 @@ downloadBtn.addEventListener('click', async () => {
// Add Date Format and Keystroke Actions to the FIELD (not widget) // Add Date Format and Keystroke Actions to the FIELD (not widget)
const dateFormat = field.dateFormat || 'mm/dd/yyyy' const dateFormat = field.dateFormat || 'mm/dd/yyyy'
const formatAction = pdfDoc.context.obj({ const formatAction = pdfDoc.context.obj({
Type: 'Action', Type: 'Action',
S: 'JavaScript', S: 'JavaScript',
@@ -1707,6 +1811,9 @@ downloadBtn.addEventListener('click', async () => {
const pdfBytes = await pdfDoc.save() const pdfBytes = await pdfDoc.save()
const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }) const blob = new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' })
downloadFile(blob, 'fillable-form.pdf') downloadFile(blob, 'fillable-form.pdf')
showModal('Success', 'Your PDF has been downloaded successfully.', 'info', () => {
resetToInitial()
}, 'Okay')
} catch (error) { } catch (error) {
console.error('Error generating PDF:', error) console.error('Error generating PDF:', error)
const errorMessage = (error as Error).message const errorMessage = (error as Error).message
@@ -1716,7 +1823,12 @@ downloadBtn.addEventListener('click', async () => {
// Extract the field name from the error message // Extract the field name from the error message
const match = errorMessage.match(/A field already exists with the specified name: "(.+?)"/) const match = errorMessage.match(/A field already exists with the specified name: "(.+?)"/)
const fieldName = match ? match[1] : 'unknown' const fieldName = match ? match[1] : 'unknown'
showModal('Duplicate Field Name', `A field named "${fieldName}" already exists. Please rename this field to use a unique name before downloading.`, 'error')
if (existingRadioGroups.has(fieldName)) {
console.log(`Adding to existing radio group: ${fieldName}`)
} else {
showModal('Duplicate Field Name', `A field named "${fieldName}" already exists. Please rename this field to use a unique name before downloading.`, 'error')
}
} else { } else {
showModal('Error', 'Error generating PDF: ' + errorMessage, 'error') showModal('Error', 'Error generating PDF: ' + errorMessage, 'error')
} }
@@ -1805,7 +1917,7 @@ function switchToPage(pageIndex: number): void {
} }
// Render the canvas for the current page // Render the canvas for the current page
function renderCanvas(): void { async function renderCanvas(): Promise<void> {
const currentPage = pages[currentPageIndex] const currentPage = pages[currentPageIndex]
if (!currentPage) return if (!currentPage) return
@@ -1814,6 +1926,7 @@ function renderCanvas(): void {
currentScale = scale currentScale = scale
// Use actual PDF page dimensions (not scaled)
const canvasWidth = currentPage.width * scale const canvasWidth = currentPage.width * scale
const canvasHeight = currentPage.height * scale const canvasHeight = currentPage.height * scale
@@ -1822,20 +1935,135 @@ function renderCanvas(): void {
canvas.innerHTML = '' canvas.innerHTML = ''
if (currentPage.pdfPageData) { if (uploadedPdfDoc) {
const img = document.createElement('img') try {
img.src = currentPage.pdfPageData as string const arrayBuffer = await uploadedPdfDoc.save()
img.style.position = 'absolute' const blob = new Blob([arrayBuffer.buffer as ArrayBuffer], { type: 'application/pdf' })
img.style.top = '0' const blobUrl = URL.createObjectURL(blob)
img.style.left = '0'
img.style.width = '100%' const iframe = document.createElement('iframe')
img.style.height = '100%' iframe.src = `/pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}#page=${currentPageIndex + 1}&toolbar=0`
img.style.pointerEvents = 'none' iframe.style.width = '100%'
img.style.opacity = '0.8' // Slightly transparent so fields are visible iframe.style.height = `${canvasHeight}px`
canvas.appendChild(img) iframe.style.border = 'none'
iframe.style.position = 'absolute'
iframe.style.top = '0'
iframe.style.left = '0'
iframe.style.pointerEvents = 'none'
iframe.style.opacity = '0.8'
iframe.onload = () => {
try {
const viewerWindow = iframe.contentWindow as any
if (viewerWindow && viewerWindow.PDFViewerApplication) {
const app = viewerWindow.PDFViewerApplication
const style = viewerWindow.document.createElement('style')
style.textContent = `
* {
margin: 0 !important;
padding: 0 !important;
}
html, body {
margin: 0 !important;
padding: 0 !important;
background-color: transparent !important;
overflow: hidden !important;
}
#toolbarContainer {
display: none !important;
}
#mainContainer {
top: 0 !important;
position: absolute !important;
left: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
#outerContainer {
background-color: transparent !important;
margin: 0 !important;
padding: 0 !important;
}
#viewerContainer {
top: 0 !important;
background-color: transparent !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
}
.toolbar {
display: none !important;
}
.pdfViewer {
padding: 0 !important;
margin: 0 !important;
}
.page {
margin: 0 !important;
padding: 0 !important;
border: none !important;
box-shadow: none !important;
}
`
viewerWindow.document.head.appendChild(style)
const checkRender = setInterval(() => {
if (app.pdfViewer && app.pdfViewer.pagesCount > 0) {
clearInterval(checkRender)
const pageContainer = viewerWindow.document.querySelector('.page')
if (pageContainer) {
const initialRect = pageContainer.getBoundingClientRect()
const offsetX = -initialRect.left
const offsetY = -initialRect.top
pageContainer.style.transform = `translate(${offsetX}px, ${offsetY}px)`
setTimeout(() => {
const rect = pageContainer.getBoundingClientRect()
const style = viewerWindow.getComputedStyle(pageContainer)
const borderLeft = parseFloat(style.borderLeftWidth) || 0
const borderTop = parseFloat(style.borderTopWidth) || 0
const borderRight = parseFloat(style.borderRightWidth) || 0
pdfViewerOffset = {
x: rect.left + borderLeft,
y: rect.top + borderTop
}
const contentWidth = rect.width - borderLeft - borderRight
pdfViewerScale = contentWidth / currentPage.width
console.log('📏 Calibrated Metrics (force positioned):', {
initialPosition: { left: initialRect.left, top: initialRect.top },
appliedTransform: { x: offsetX, y: offsetY },
finalRect: { left: rect.left, top: rect.top, width: rect.width, height: rect.height },
computedBorders: { left: borderLeft, top: borderTop, right: borderRight },
finalOffset: pdfViewerOffset,
finalScale: pdfViewerScale,
pdfDimensions: { width: currentPage.width, height: currentPage.height }
})
}, 50)
}
}
}, 100)
}
} catch (e) {
console.error('Error accessing iframe content:', e)
}
}
canvas.appendChild(iframe)
console.log('Canvas dimensions:', { width: canvasWidth, height: canvasHeight, scale: currentScale })
console.log('PDF page dimensions:', { width: currentPage.width, height: currentPage.height })
} catch (error) {
console.error('Error rendering PDF:', error)
}
} }
// Render fields for current page
fields.filter(f => f.pageIndex === currentPageIndex).forEach(field => { fields.filter(f => f.pageIndex === currentPageIndex).forEach(field => {
renderField(field) renderField(field)
}) })
@@ -1904,25 +2132,41 @@ async function handlePdfUpload(file: File) {
uploadedPdfDoc = await PDFDocument.load(arrayBuffer) uploadedPdfDoc = await PDFDocument.load(arrayBuffer)
// Check for existing fields and update counter // Check for existing fields and update counter
existingFieldNames.clear()
try { try {
const form = uploadedPdfDoc.getForm() const form = uploadedPdfDoc.getForm()
const fields = form.getFields() const pdfFields = form.getFields()
fields.forEach(field => {
// console.log('📋 Found', pdfFields.length, 'existing fields in uploaded PDF')
pdfFields.forEach(field => {
const name = field.getName() const name = field.getName()
// Check if name matches pattern Type_Number existingFieldNames.add(name) // Track all existing field names
if (field instanceof PDFRadioGroup) {
existingRadioGroups.add(name)
}
// console.log(' Field:', name, '| Type:', field.constructor.name)
const match = name.match(/([a-zA-Z]+)_(\d+)/) const match = name.match(/([a-zA-Z]+)_(\d+)/)
if (match) { if (match) {
const num = parseInt(match[2]) const num = parseInt(match[2])
if (!isNaN(num) && num >= fieldCounter) { if (!isNaN(num) && num > fieldCounter) {
fieldCounter = num fieldCounter = num
console.log(' → Updated field counter to:', fieldCounter)
} }
} }
}) })
// TODO@ALAM: DEBUGGER
// console.log('Field counter after upload:', fieldCounter)
// console.log('Existing field names:', Array.from(existingFieldNames))
} catch (e) { } catch (e) {
// No form or error getting fields, ignore console.log('No form fields found or error reading fields:', e)
} }
const pdfjsDoc = await getPDFDocument({ data: arrayBuffer }).promise uploadedPdfjsDoc = await getPDFDocument({ data: arrayBuffer }).promise
const pageCount = uploadedPdfDoc.getPageCount() const pageCount = uploadedPdfDoc.getPageCount()
pages = [] pages = []
@@ -1931,27 +2175,11 @@ async function handlePdfUpload(file: File) {
const page = uploadedPdfDoc.getPage(i) const page = uploadedPdfDoc.getPage(i)
const { width, height } = page.getSize() const { width, height } = page.getSize()
const pdfjsPage = await pdfjsDoc.getPage(i + 1)
const viewport = pdfjsPage.getViewport({ scale: 1.333 })
const tempCanvas = document.createElement('canvas')
tempCanvas.width = viewport.width
tempCanvas.height = viewport.height
const context = tempCanvas.getContext('2d')!
await pdfjsPage.render({
canvasContext: context,
viewport,
canvas: tempCanvas,
}).promise
const dataUrl = tempCanvas.toDataURL('image/png')
pages.push({ pages.push({
index: i, index: i,
width, width,
height, height,
pdfPageData: dataUrl as any pdfPageData: undefined
}) })
} }
@@ -2005,17 +2233,26 @@ const errorModalTitle = document.getElementById('errorModalTitle')
const errorModalMessage = document.getElementById('errorModalMessage') const errorModalMessage = document.getElementById('errorModalMessage')
const errorModalClose = document.getElementById('errorModalClose') const errorModalClose = document.getElementById('errorModalClose')
function showModal(title: string, message: string, type: 'error' | 'warning' | 'info' = 'error') { let modalCloseCallback: (() => void) | null = null
if (!errorModal || !errorModalTitle || !errorModalMessage) return
function showModal(title: string, message: string, type: 'error' | 'warning' | 'info' = 'error', onClose?: () => void, buttonText: string = 'Close') {
if (!errorModal || !errorModalTitle || !errorModalMessage || !errorModalClose) return
errorModalTitle.textContent = title errorModalTitle.textContent = title
errorModalMessage.textContent = message errorModalMessage.textContent = message
errorModalClose.textContent = buttonText
modalCloseCallback = onClose || null
errorModal.classList.remove('hidden') errorModal.classList.remove('hidden')
} }
if (errorModalClose) { if (errorModalClose) {
errorModalClose.addEventListener('click', () => { errorModalClose.addEventListener('click', () => {
errorModal?.classList.add('hidden') errorModal?.classList.add('hidden')
if (modalCloseCallback) {
modalCloseCallback()
modalCloseCallback = null
}
}) })
} }
@@ -2024,6 +2261,10 @@ if (errorModal) {
errorModal.addEventListener('click', (e) => { errorModal.addEventListener('click', (e) => {
if (e.target === errorModal) { if (e.target === errorModal) {
errorModal.classList.add('hidden') errorModal.classList.add('hidden')
if (modalCloseCallback) {
modalCloseCallback()
modalCloseCallback = null
}
} }
}) })
} }
+4 -2
View File
@@ -18,7 +18,7 @@ import { pdfToPng } from './pdf-to-png.js';
import { pngToPdf } from './png-to-pdf.js'; import { pngToPdf } from './png-to-pdf.js';
import { pdfToWebp } from './pdf-to-webp.js'; import { pdfToWebp } from './pdf-to-webp.js';
import { webpToPdf } from './webp-to-pdf.js'; import { webpToPdf } from './webp-to-pdf.js';
import { deletePages } from './delete-pages.js'; import { deletePages, setupDeletePagesTool } from './delete-pages.js';
import { addBlankPage } from './add-blank-page.js'; import { addBlankPage } from './add-blank-page.js';
import { extractPages } from './extract-pages.js'; import { extractPages } from './extract-pages.js';
import { addWatermark, setupWatermarkUI } from './add-watermark.js'; import { addWatermark, setupWatermarkUI } from './add-watermark.js';
@@ -67,6 +67,7 @@ import { extractAttachments } from './extract-attachments.js';
import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js'; import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
import { sanitizePdf } from './sanitize-pdf.js'; import { sanitizePdf } from './sanitize-pdf.js';
import { removeRestrictions } from './remove-restrictions.js'; import { removeRestrictions } from './remove-restrictions.js';
import { repairPdf } from './repair-pdf.js';
export const toolLogic = { export const toolLogic = {
merge: { process: merge, setup: setupMergeTool }, merge: { process: merge, setup: setupMergeTool },
@@ -74,6 +75,7 @@ export const toolLogic = {
encrypt, encrypt,
decrypt, decrypt,
'remove-restrictions': removeRestrictions, 'remove-restrictions': removeRestrictions,
'repair-pdf': repairPdf,
organize, organize,
rotate, rotate,
'add-page-numbers': addPageNumbers, 'add-page-numbers': addPageNumbers,
@@ -90,7 +92,7 @@ export const toolLogic = {
'png-to-pdf': pngToPdf, 'png-to-pdf': pngToPdf,
'pdf-to-webp': pdfToWebp, 'pdf-to-webp': pdfToWebp,
'webp-to-pdf': webpToPdf, 'webp-to-pdf': webpToPdf,
'delete-pages': deletePages, 'delete-pages': { process: deletePages, setup: setupDeletePagesTool },
'add-blank-page': addBlankPage, 'add-blank-page': addBlankPage,
'extract-pages': extractPages, 'extract-pages': extractPages,
'add-watermark': { process: addWatermark, setup: setupWatermarkUI }, 'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
+143 -115
View File
@@ -1,17 +1,30 @@
import { showLoader, hideLoader, showAlert } from '../ui.ts'; import { showLoader, hideLoader, showAlert } from '../ui.ts';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.ts'; import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.ts';
import { state } from '../state.ts'; import { state } from '../state.ts';
import { renderPagesProgressively, cleanupLazyRendering, createPlaceholder } from '../utils/render-utils.ts'; import { renderPagesProgressively, cleanupLazyRendering } from '../utils/render-utils.ts';
import { createIcons, icons } from 'lucide'; import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString(); pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const mergeState = { interface MergeState {
pdfDocs: Record<string, any>;
pdfBytes: Record<string, ArrayBuffer>;
activeMode: 'file' | 'page';
sortableInstances: {
fileList?: Sortable;
pageThumbnails?: Sortable;
};
isRendering: boolean;
cachedThumbnails: boolean | null;
lastFileHash: string | null;
}
const mergeState: MergeState = {
pdfDocs: {}, pdfDocs: {},
pdfBytes: {},
activeMode: 'file', activeMode: 'file',
sortableInstances: {}, sortableInstances: {},
isRendering: false, isRendering: false,
@@ -19,47 +32,16 @@ const mergeState = {
lastFileHash: null, lastFileHash: null,
}; };
function parsePageRanges(rangeString: any, totalPages: any) { const mergeWorker = new Worker('/workers/merge.worker.js');
const indices = new Set();
if (!rangeString.trim()) return [];
const ranges = rangeString.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
)
continue;
for (let i = start; i <= end; i++) {
indices.add(i - 1);
}
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indices.add(pageNum - 1);
}
}
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
return Array.from(indices).sort((a, b) => a - b);
}
function initializeFileListSortable() { function initializeFileListSortable() {
const fileList = document.getElementById('file-list'); const fileList = document.getElementById('file-list');
if (!fileList) return; if (!fileList) return;
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
if (mergeState.sortableInstances.fileList) { if (mergeState.sortableInstances.fileList) {
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
mergeState.sortableInstances.fileList.destroy(); mergeState.sortableInstances.fileList.destroy();
} }
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
mergeState.sortableInstances.fileList = Sortable.create(fileList, { mergeState.sortableInstances.fileList = Sortable.create(fileList, {
handle: '.drag-handle', handle: '.drag-handle',
animation: 150, animation: 150,
@@ -79,13 +61,10 @@ function initializePageThumbnailsSortable() {
const container = document.getElementById('page-merge-preview'); const container = document.getElementById('page-merge-preview');
if (!container) return; if (!container) return;
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
if (mergeState.sortableInstances.pageThumbnails) { if (mergeState.sortableInstances.pageThumbnails) {
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
mergeState.sortableInstances.pageThumbnails.destroy(); mergeState.sortableInstances.pageThumbnails.destroy();
} }
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, { mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
animation: 150, animation: 150,
ghostClass: 'sortable-ghost', ghostClass: 'sortable-ghost',
@@ -128,13 +107,13 @@ async function renderPageMergeThumbnails() {
mergeState.isRendering = true; mergeState.isRendering = true;
container.textContent = ''; container.textContent = '';
// Cleanup any previous lazy loading observers
cleanupLazyRendering(); cleanupLazyRendering();
let totalPages = state.files.reduce((sum, file) => { let totalPages = 0;
const pdfDoc = mergeState.pdfDocs[file.name]; for (const file of state.files) {
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0); const doc = mergeState.pdfDocs[file.name];
}, 0); if (doc) totalPages += doc.numPages;
}
try { try {
let currentPageNumber = 0; let currentPageNumber = 0;
@@ -176,11 +155,8 @@ async function renderPageMergeThumbnails() {
// Render pages from all files progressively // Render pages from all files progressively
for (const file of state.files) { for (const file of state.files) {
const pdfDoc = mergeState.pdfDocs[file.name]; const pdfjsDoc = mergeState.pdfDocs[file.name];
if (!pdfDoc) continue; if (!pdfjsDoc) continue;
const pdfData = await pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfData }).promise;
// Create a wrapper function that includes the file name // Create a wrapper function that includes the file name
const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => { const createWrapperWithFileName = (canvas: HTMLCanvasElement, pageNumber: number) => {
@@ -193,7 +169,7 @@ async function renderPageMergeThumbnails() {
container, container,
createWrapperWithFileName, createWrapperWithFileName,
{ {
batchSize: 6, batchSize: 8,
useLazyLoading: true, useLazyLoading: true,
lazyLoadMargin: '300px', lazyLoadMargin: '300px',
onProgress: (current, total) => { onProgress: (current, total) => {
@@ -207,10 +183,6 @@ async function renderPageMergeThumbnails() {
} }
} }
); );
// TODO@ALAM - DON'T destroy the PDF.js document here - lazy loading still needs it!
// It will be garbage collected automatically when no longer referenced
// pdfjsDoc.destroy(); // REMOVED
} }
mergeState.cachedThumbnails = true; mergeState.cachedThumbnails = true;
@@ -229,91 +201,159 @@ async function renderPageMergeThumbnails() {
export async function merge() { export async function merge() {
showLoader('Merging PDFs...'); showLoader('Merging PDFs...');
try { try {
const newPdfDoc = await PDFLibDocument.create(); const jobs: MergeJob[] = [];
const filesToMerge: MergeFile[] = [];
const uniqueFileNames = new Set<string>();
if (mergeState.activeMode === 'file') { if (mergeState.activeMode === 'file') {
const fileList = document.getElementById('file-list'); const fileList = document.getElementById('file-list');
if (!fileList) throw new Error('File list not found');
const sortedFiles = Array.from(fileList.children) const sortedFiles = Array.from(fileList.children)
.map((li) => { .map((li) => {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message return state.files.find((f) => f.name === (li as HTMLElement).dataset.fileName);
return state.files.find((f) => f.name === li.dataset.fileName);
}) })
.filter(Boolean); .filter(Boolean);
for (const file of sortedFiles) { for (const file of sortedFiles) {
if (!file) continue;
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_'); const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
const rangeInput = document.getElementById(`range-${safeFileName}`); const rangeInput = document.getElementById(`range-${safeFileName}`) as HTMLInputElement;
if (!rangeInput) continue;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message uniqueFileNames.add(file.name);
const rangeInputValue = rangeInput.value;
const sourcePdf = mergeState.pdfDocs[file.name];
if (!sourcePdf) continue;
const totalPages = sourcePdf.getPageCount(); if (rangeInput && rangeInput.value.trim()) {
const pageIndices = parsePageRanges(rangeInputValue, totalPages); jobs.push({
fileName: file.name,
const indicesToCopy = rangeType: 'specific',
pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices(); rangeString: rangeInput.value.trim()
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy); });
copiedPages.forEach((page: any) => newPdfDoc.addPage(page)); } else {
jobs.push({
fileName: file.name,
rangeType: 'all'
});
}
} }
} else { } else {
// Page Mode
const pageContainer = document.getElementById('page-merge-preview'); const pageContainer = document.getElementById('page-merge-preview');
if (!pageContainer) throw new Error('Page container not found');
const pageElements = Array.from(pageContainer.children); const pageElements = Array.from(pageContainer.children);
const rawPages: { fileName: string; pageIndex: number }[] = [];
for (const el of pageElements) { for (const el of pageElements) {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message const element = el as HTMLElement;
const fileName = el.dataset.fileName; const fileName = element.dataset.fileName;
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message const pageIndex = parseInt(element.dataset.pageIndex || '', 10); // 0-based index from dataset
const pageIndex = parseInt(el.dataset.pageIndex, 10);
const sourcePdf = mergeState.pdfDocs[fileName]; if (fileName && !isNaN(pageIndex)) {
if (sourcePdf && !isNaN(pageIndex)) { uniqueFileNames.add(fileName);
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [ rawPages.push({ fileName, pageIndex });
pageIndex, }
]); }
newPdfDoc.addPage(copiedPage);
// Group contiguous pages
for (let i = 0; i < rawPages.length; i++) {
const current = rawPages[i];
let endPage = current.pageIndex;
while (
i + 1 < rawPages.length &&
rawPages[i + 1].fileName === current.fileName &&
rawPages[i + 1].pageIndex === endPage + 1
) {
endPage++;
i++;
}
if (endPage === current.pageIndex) {
// Single page
jobs.push({
fileName: current.fileName,
rangeType: 'single',
pageIndex: current.pageIndex
});
} else {
// Range of pages
jobs.push({
fileName: current.fileName,
rangeType: 'range',
startPage: current.pageIndex + 1,
endPage: endPage + 1
});
} }
} }
} }
const mergedPdfBytes = await newPdfDoc.save(); if (jobs.length === 0) {
downloadFile( showAlert('Error', 'No files or pages selected to merge.');
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), hideLoader();
'merged.pdf' return;
); }
showAlert('Success', 'PDFs merged successfully!');
for (const name of uniqueFileNames) {
const bytes = mergeState.pdfBytes[name];
if (bytes) {
filesToMerge.push({ name, data: bytes });
}
}
const message: MergeMessage = {
command: 'merge',
files: filesToMerge,
jobs: jobs
};
mergeWorker.postMessage(message, filesToMerge.map(f => f.data));
mergeWorker.onmessage = (e: MessageEvent<MergeResponse>) => {
hideLoader();
if (e.data.status === 'success') {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
downloadFile(blob, 'merged.pdf');
showAlert('Success', 'PDFs merged successfully!');
} else {
console.error('Worker merge error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to merge PDFs.');
}
};
mergeWorker.onerror = (e) => {
hideLoader();
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) { } catch (e) {
console.error('Merge error:', e); console.error('Merge error:', e);
showAlert( showAlert(
'Error', 'Error',
'Failed to merge PDFs. Please check that all files are valid and not password-protected.' 'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
); );
} finally {
hideLoader(); hideLoader();
} }
} }
export async function setupMergeTool() { export async function setupMergeTool() {
document.getElementById('merge-options').classList.remove('hidden'); document.getElementById('merge-options')?.classList.remove('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
document.getElementById('process-btn').disabled = false; if (processBtn) processBtn.disabled = false;
const wasInPageMode = mergeState.activeMode === 'page'; const wasInPageMode = mergeState.activeMode === 'page';
showLoader('Loading PDF documents...'); showLoader('Loading PDF documents...');
try { try {
mergeState.pdfDocs = {};
mergeState.pdfBytes = {};
for (const file of state.files) { for (const file of state.files) {
if (!mergeState.pdfDocs[file.name]) { const pdfBytes = await readFileAsArrayBuffer(file);
const pdfBytes = await readFileAsArrayBuffer(file); mergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(
pdfBytes as ArrayBuffer, const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
{ const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
ignoreEncryption: true, mergeState.pdfDocs[file.name] = pdfjsDoc;
}
);
}
} }
} catch (error) { } catch (error) {
console.error('Error loading PDFs:', error); console.error('Error loading PDFs:', error);
@@ -329,10 +369,12 @@ export async function setupMergeTool() {
const pagePanel = document.getElementById('page-mode-panel'); const pagePanel = document.getElementById('page-mode-panel');
const fileList = document.getElementById('file-list'); const fileList = document.getElementById('file-list');
if (!fileModeBtn || !pageModeBtn || !filePanel || !pagePanel || !fileList) return;
fileList.textContent = ''; // Clear list safely fileList.textContent = ''; // Clear list safely
(state.files as File[]).forEach((f) => { (state.files as File[]).forEach((f) => {
const doc = mergeState.pdfDocs[f.name]; const doc = mergeState.pdfDocs[f.name];
const pageCount = doc ? doc.getPageCount() : 'N/A'; const pageCount = doc ? doc.numPages : 'N/A';
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_'); const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
const li = document.createElement('li'); const li = document.createElement('li');
@@ -377,8 +419,8 @@ export async function setupMergeTool() {
initializeFileListSortable(); initializeFileListSortable();
const newFileModeBtn = fileModeBtn.cloneNode(true); const newFileModeBtn = fileModeBtn.cloneNode(true) as HTMLElement;
const newPageModeBtn = pageModeBtn.cloneNode(true); const newPageModeBtn = pageModeBtn.cloneNode(true) as HTMLElement;
fileModeBtn.replaceWith(newFileModeBtn); fileModeBtn.replaceWith(newFileModeBtn);
pageModeBtn.replaceWith(newPageModeBtn); pageModeBtn.replaceWith(newPageModeBtn);
@@ -389,13 +431,9 @@ export async function setupMergeTool() {
filePanel.classList.remove('hidden'); filePanel.classList.remove('hidden');
pagePanel.classList.add('hidden'); pagePanel.classList.add('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-indigo-600', 'text-white'); newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white'); newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300'); newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
}); });
@@ -406,13 +444,9 @@ export async function setupMergeTool() {
filePanel.classList.add('hidden'); filePanel.classList.add('hidden');
pagePanel.classList.remove('hidden'); pagePanel.classList.remove('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-indigo-600', 'text-white'); newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white'); newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300'); newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
await renderPageMergeThumbnails(); await renderPageMergeThumbnails();
@@ -423,20 +457,14 @@ export async function setupMergeTool() {
filePanel.classList.add('hidden'); filePanel.classList.add('hidden');
pagePanel.classList.remove('hidden'); pagePanel.classList.remove('hidden');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-indigo-600', 'text-white'); newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300'); newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white'); newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300'); newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
await renderPageMergeThumbnails(); await renderPageMergeThumbnails();
} else { } else {
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newFileModeBtn.classList.add('bg-indigo-600', 'text-white'); newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300'); newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
} }
} }
+23 -3
View File
@@ -6,6 +6,7 @@ import Sortable from 'sortablejs';
import { downloadFile, getPDFDocument } from '../utils/helpers'; import { downloadFile, getPDFDocument } from '../utils/helpers';
import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils'; import { renderPagesProgressively, cleanupLazyRendering, renderPageToCanvas, createPlaceholder } from '../utils/render-utils';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
import { repairPdfFile } from './repair-pdf.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
@@ -351,8 +352,27 @@ async function loadPdfs(files: File[]) {
if (renderCancelled) break; if (renderCancelled) break;
try { try {
const arrayBuffer = await file.arrayBuffer(); let arrayBuffer: ArrayBuffer;
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
try {
console.log(`Repairing ${file.name}...`);
const loadingText = document.getElementById('loading-text');
if (loadingText) loadingText.textContent = `Repairing ${file.name}...`;
const repairedData = await repairPdfFile(file);
if (repairedData) {
arrayBuffer = repairedData.buffer as ArrayBuffer;
console.log(`Successfully repaired ${file.name} before loading.`);
} else {
console.warn(`Repair returned null for ${file.name}, using original file.`);
arrayBuffer = await file.arrayBuffer();
}
} catch (repairError) {
console.warn(`Failed to repair ${file.name}, attempting to load original:`, repairError);
arrayBuffer = await file.arrayBuffer();
}
const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
currentPdfDocs.push(pdfDoc); currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1; const pdfIndex = currentPdfDocs.length - 1;
@@ -735,7 +755,7 @@ async function handleInsertPdf(e: Event) {
try { try {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer); const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
currentPdfDocs.push(pdfDoc); currentPdfDocs.push(pdfDoc);
const pdfIndex = currentPdfDocs.length - 1; const pdfIndex = currentPdfDocs.length - 1;
+92
View File
@@ -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();
});
+128
View File
@@ -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.');
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ export async function reversePages() {
for (let j = 0; j < pdfDocs.length; j++) { for (let j = 0; j < pdfDocs.length; j++) {
const file = pdfDocs[j]; const file = pdfDocs[j];
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const pdfDoc = await PDFLibDocument.load(arrayBuffer); const pdfDoc = await PDFLibDocument.load(arrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
const newPdf = await PDFLibDocument.create(); const newPdf = await PDFLibDocument.create();
const pageCount = pdfDoc.getPageCount(); const pageCount = pdfDoc.getPageCount();
const reversedIndices = Array.from( const reversedIndices = Array.from(
+42 -11
View File
@@ -3,25 +3,56 @@ import { downloadFile, resetAndReloadTool } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { getRotationState, resetRotationState } from '../handlers/fileHandler.js'; import { getRotationState, resetRotationState } from '../handlers/fileHandler.js';
import { degrees } from 'pdf-lib'; import { PDFDocument, degrees } from 'pdf-lib';
export async function rotate() { export async function rotate() {
showLoader('Applying rotations...'); showLoader('Applying rotations...');
try { try {
const pages = state.pdfDoc.getPages(); const originalPdf = state.pdfDoc;
const pageCount = originalPdf.getPageCount();
const rotationStateArray = getRotationState(); const rotationStateArray = getRotationState();
// Apply rotations from state (not DOM) to ensure all pages including lazy-loaded ones are rotated const newPdfDoc = await PDFDocument.create();
rotationStateArray.forEach((rotation, pageIndex) => {
if (rotation !== 0 && pages[pageIndex]) {
const currentRotation = pages[pageIndex].getRotation().angle;
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
}
});
const rotatedPdfBytes = await state.pdfDoc.save(); for (let i = 0; i < pageCount; i++) {
const rotation = rotationStateArray[i] || 0;
const originalPage = originalPdf.getPage(i);
const currentRotation = originalPage.getRotation().angle;
const totalRotation = currentRotation + rotation;
if (totalRotation % 90 === 0) {
const [copiedPage] = await newPdfDoc.copyPages(originalPdf, [i]);
copiedPage.setRotation(degrees(totalRotation));
newPdfDoc.addPage(copiedPage);
} else {
const embeddedPage = await newPdfDoc.embedPage(originalPage);
const { width, height } = embeddedPage.scale(1);
const angleRad = (totalRotation * Math.PI) / 180;
const absCos = Math.abs(Math.cos(angleRad));
const absSin = Math.abs(Math.sin(angleRad));
const newWidth = width * absCos + height * absSin;
const newHeight = width * absSin + height * absCos;
const newPage = newPdfDoc.addPage([newWidth, newHeight]);
const x = newWidth / 2 - (width / 2 * Math.cos(angleRad) - height / 2 * Math.sin(angleRad));
const y = newHeight / 2 - (width / 2 * Math.sin(angleRad) + height / 2 * Math.cos(angleRad));
newPage.drawPage(embeddedPage, {
x,
y,
width,
height,
rotate: degrees(totalRotation),
});
}
}
const rotatedPdfBytes = await newPdfDoc.save();
downloadFile( downloadFile(
new Blob([rotatedPdfBytes], { type: 'application/pdf' }), new Blob([rotatedPdfBytes as any], { type: 'application/pdf' }),
'rotated.pdf' 'rotated.pdf'
); );
+19 -4
View File
@@ -80,6 +80,12 @@ const init = () => {
supportSection.style.display = 'none'; supportSection.style.display = 'none';
} }
// Hide "Used by companies" section
const usedBySection = document.querySelector('.hide-section') as HTMLElement;
if (usedBySection) {
usedBySection.style.display = 'none';
}
// Hide footer but keep copyright // Hide footer but keep copyright
const footer = document.querySelector('footer'); const footer = document.querySelector('footer');
if (footer) { if (footer) {
@@ -291,17 +297,26 @@ const init = () => {
console.log('Please share our tool and share the love!'); console.log('Please share our tool and share the love!');
const githubStarsElement = document.getElementById('github-stars'); const githubStarsElements = [
if (githubStarsElement && !__SIMPLE_MODE__) { document.getElementById('github-stars-desktop'),
document.getElementById('github-stars-mobile')
];
if (githubStarsElements.some(el => el) && !__SIMPLE_MODE__) {
fetch('https://api.github.com/repos/alam00000/bentopdf') fetch('https://api.github.com/repos/alam00000/bentopdf')
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then((data) => {
if (data.stargazers_count !== undefined) { if (data.stargazers_count !== undefined) {
githubStarsElement.textContent = formatStars(data.stargazers_count); const formattedStars = formatStars(data.stargazers_count);
githubStarsElements.forEach(el => {
if (el) el.textContent = formattedStars;
});
} }
}) })
.catch(() => { .catch(() => {
githubStarsElement.textContent = '-'; githubStarsElements.forEach(el => {
if (el) el.textContent = '-';
});
}); });
} }
+174 -40
View File
@@ -44,19 +44,23 @@ export const dom = {
}; };
export const showLoader = (text = 'Processing...') => { export const showLoader = (text = 'Processing...') => {
dom.loaderText.textContent = text; if (dom.loaderText) dom.loaderText.textContent = text;
dom.loaderModal.classList.remove('hidden'); if (dom.loaderModal) dom.loaderModal.classList.remove('hidden');
}; };
export const hideLoader = () => dom.loaderModal.classList.add('hidden'); export const hideLoader = () => {
if (dom.loaderModal) dom.loaderModal.classList.add('hidden');
};
export const showAlert = (title: any, message: any) => { export const showAlert = (title: any, message: any) => {
dom.alertTitle.textContent = title; if (dom.alertTitle) dom.alertTitle.textContent = title;
dom.alertMessage.textContent = message; if (dom.alertMessage) dom.alertMessage.textContent = message;
dom.alertModal.classList.remove('hidden'); if (dom.alertModal) dom.alertModal.classList.remove('hidden');
}; };
export const hideAlert = () => dom.alertModal.classList.add('hidden'); export const hideAlert = () => {
if (dom.alertModal) dom.alertModal.classList.add('hidden');
};
export const switchView = (view: any) => { export const switchView = (view: any) => {
if (view === 'grid') { if (view === 'grid') {
@@ -125,7 +129,7 @@ function initializeOrganizeSortable(containerId: any) {
* @param {object} pdfDoc The loaded pdf-lib document instance. * @param {object} pdfDoc The loaded pdf-lib document instance.
*/ */
export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => { export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
const containerId = toolId === 'organize' ? 'page-organizer' : 'page-rotator'; const containerId = toolId === 'organize' ? 'page-organizer' : toolId === 'delete-pages' ? 'delete-pages-preview' : 'page-rotator';
const container = document.getElementById(containerId); const container = document.getElementById(containerId);
if (!container) return; if (!container) return;
@@ -134,6 +138,9 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
// Cleanup any previous lazy loading observers // Cleanup any previous lazy loading observers
cleanupLazyRendering(); cleanupLazyRendering();
const currentRenderId = Date.now();
container.dataset.renderId = currentRenderId.toString();
showLoader('Rendering page previews...'); showLoader('Rendering page previews...');
const pdfData = await pdfDoc.save(); const pdfData = await pdfDoc.save();
@@ -185,7 +192,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
wrapper.append(pageNumSpan, deleteBtn); wrapper.append(pageNumSpan, deleteBtn);
} else if (toolId === 'rotate') { } else if (toolId === 'rotate') {
wrapper.className = 'page-rotator-item flex flex-col items-center gap-2'; wrapper.className = 'page-rotator-item flex flex-col items-center gap-2 relative group';
// Read rotation from state (handles "Rotate All" on lazy-loaded pages) // Read rotation from state (handles "Rotate All" on lazy-loaded pages)
const rotationStateArray = getRotationState(); const rotationStateArray = getRotationState();
@@ -202,35 +209,122 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
wrapper.appendChild(imgContainer); wrapper.appendChild(imgContainer);
const controlsDiv = document.createElement('div'); // Page Number Overlay (Top Left)
controlsDiv.className = 'flex items-center justify-center gap-3 w-full';
const pageNumSpan = document.createElement('span'); const pageNumSpan = document.createElement('span');
pageNumSpan.className = 'font-medium text-sm text-white'; pageNumSpan.className =
'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none';
pageNumSpan.textContent = pageNumber.toString(); pageNumSpan.textContent = pageNumber.toString();
wrapper.appendChild(pageNumSpan);
const rotateBtn = document.createElement('button'); const controlsDiv = document.createElement('div');
rotateBtn.className = controlsDiv.className = 'flex flex-col lg:flex-row items-center justify-center w-full gap-2 px-1';
'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
rotateBtn.title = 'Rotate 90°'; // Custom Stepper Component
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>'; const stepperContainer = document.createElement('div');
rotateBtn.addEventListener('click', (e) => { stepperContainer.className = 'flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden w-24 h-8';
e.stopPropagation();
const card = (e.currentTarget as HTMLElement).closest( const decrementBtn = document.createElement('button');
'.page-rotator-item' decrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center';
) as HTMLElement; decrementBtn.innerHTML = '<i data-lucide="minus" class="w-3 h-3"></i>';
const angleInput = document.createElement('input');
angleInput.type = 'number';
angleInput.className = 'no-spinner w-full h-full bg-transparent text-white text-xs text-center focus:outline-none appearance-none m-0 p-0 border-none';
angleInput.value = initialRotation.toString();
angleInput.placeholder = "0";
const incrementBtn = document.createElement('button');
incrementBtn.className = 'px-2 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center';
incrementBtn.innerHTML = '<i data-lucide="plus" class="w-3 h-3"></i>';
// Helper to update rotation
const updateRotation = (newRotation: number) => {
const card = wrapper; // Closure capture
const imgEl = card.querySelector('img'); const imgEl = card.querySelector('img');
const pageIndex = pageNumber - 1; const pageIndex = pageNumber - 1;
let currentRotation = parseInt(card.dataset.rotation);
currentRotation = (currentRotation + 90) % 360;
card.dataset.rotation = currentRotation.toString();
imgEl.style.transform = `rotate(${currentRotation}deg)`;
updateRotationState(pageIndex, currentRotation); // Update UI
angleInput.value = newRotation.toString();
card.dataset.rotation = newRotation.toString();
imgEl.style.transform = `rotate(${newRotation}deg)`;
// Update State
updateRotationState(pageIndex, newRotation);
};
// Event Listeners
decrementBtn.addEventListener('click', (e) => {
e.stopPropagation();
let current = parseInt(angleInput.value) || 0;
updateRotation(current - 1);
}); });
controlsDiv.append(pageNumSpan, rotateBtn); incrementBtn.addEventListener('click', (e) => {
e.stopPropagation();
let current = parseInt(angleInput.value) || 0;
updateRotation(current + 1);
});
angleInput.addEventListener('change', (e) => {
e.stopPropagation();
let val = parseInt((e.target as HTMLInputElement).value) || 0;
updateRotation(val);
});
angleInput.addEventListener('click', (e) => e.stopPropagation());
stepperContainer.append(decrementBtn, angleInput, incrementBtn);
const rotateBtn = document.createElement('button');
rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-1.5 rounded-md text-gray-200 transition-colors flex-shrink-0';
rotateBtn.title = 'Rotate +90°';
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-4 h-4"></i>';
rotateBtn.addEventListener('click', (e) => {
e.stopPropagation();
let current = parseInt(angleInput.value) || 0;
updateRotation(current + 90);
});
controlsDiv.append(stepperContainer, rotateBtn);
wrapper.appendChild(controlsDiv); wrapper.appendChild(controlsDiv);
} else if (toolId === 'delete-pages') {
wrapper.className = 'page-thumbnail relative group cursor-pointer transition-all duration-200';
wrapper.dataset.pageNumber = pageNumber.toString();
const innerContainer = document.createElement('div');
innerContainer.className = 'relative w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600 transition-colors duration-200';
innerContainer.appendChild(img);
wrapper.appendChild(innerContainer);
const pageNumSpan = document.createElement('span');
pageNumSpan.className =
'absolute top-2 left-2 bg-gray-900 bg-opacity-75 text-white text-xs font-medium rounded-md px-2 py-1 shadow-sm z-10 pointer-events-none';
pageNumSpan.textContent = pageNumber.toString();
wrapper.appendChild(pageNumSpan);
wrapper.addEventListener('click', () => {
const input = document.getElementById('pages-to-delete') as HTMLInputElement;
if (!input) return;
const currentVal = input.value;
let pages = currentVal.split(',').map(s => s.trim()).filter(s => s);
const pageStr = pageNumber.toString();
if (pages.includes(pageStr)) {
pages = pages.filter(p => p !== pageStr);
} else {
pages.push(pageStr);
}
pages.sort((a, b) => {
const numA = parseInt(a.split('-')[0]);
const numB = parseInt(b.split('-')[0]);
return numA - numB;
});
input.value = pages.join(', ');
input.dispatchEvent(new Event('input'));
});
} }
return wrapper; return wrapper;
@@ -243,7 +337,7 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
container, container,
createWrapper, createWrapper,
{ {
batchSize: 6, batchSize: 8,
useLazyLoading: true, useLazyLoading: true,
lazyLoadMargin: '300px', lazyLoadMargin: '300px',
onProgress: (current, total) => { onProgress: (current, total) => {
@@ -251,12 +345,17 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
}, },
onBatchComplete: () => { onBatchComplete: () => {
createIcons({ icons }); createIcons({ icons });
},
shouldCancel: () => {
return container.dataset.renderId !== currentRenderId.toString();
} }
} }
); );
if (toolId === 'organize') { if (toolId === 'organize') {
initializeOrganizeSortable(containerId); initializeOrganizeSortable(containerId);
} else if (toolId === 'delete-pages') {
// No sortable needed for delete pages
} }
// Reinitialize lucide icons for dynamically added elements // Reinitialize lucide icons for dynamically added elements
@@ -576,15 +675,49 @@ export const toolTemplates = {
<div id="rotate-all-controls" class="hidden my-6"> <div id="rotate-all-controls" class="hidden my-6">
<div class="bg-gray-900/50 border border-gray-700 rounded-lg p-4"> <div class="bg-gray-900/50 border border-gray-700 rounded-lg p-4">
<h3 class="text-sm font-semibold text-gray-400 mb-3 text-center">BATCH ACTIONS</h3> <h3 class="text-sm font-semibold text-gray-400 mb-3 text-center">BATCH ACTIONS</h3>
<div class="flex justify-center gap-4"> <div class="flex flex-col md:flex-row justify-center gap-6 items-center">
<button id="rotate-all-left-btn" class="flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95">
<i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i> <!-- 90 Degree Rotation Group -->
Rotate All Left <div class="flex flex-col gap-2 items-center">
</button> <span class="text-xs text-gray-400 font-medium uppercase tracking-wider">Rotate By 90 degrees</span>
<button id="rotate-all-right-btn" class="flex items-center justify-center w-full px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95"> <div class="flex gap-2">
<i data-lucide="rotate-cw" class="mr-2 h-4 w-4"></i> <button id="rotate-all-left-btn" class="flex items-center justify-center px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95" title="Rotate Left 90°">
Rotate All Right <i data-lucide="rotate-ccw" class="mr-2 h-4 w-4"></i>
</button> Left
</button>
<button id="rotate-all-right-btn" class="flex items-center justify-center px-4 py-2 text-sm font-medium text-gray-200 bg-gray-800 border border-gray-600 rounded-lg shadow-sm hover:bg-gray-700 hover:border-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-500 transform transition-all duration-150 active:scale-95" title="Rotate Right 90°">
<i data-lucide="rotate-cw" class="mr-2 h-4 w-4"></i>
Right
</button>
</div>
</div>
<div class="hidden md:block w-px h-12 bg-gray-700"></div>
<!-- Custom Rotation Group -->
<div class="flex flex-col gap-2 items-center">
<span class="text-xs text-gray-400 font-medium uppercase tracking-wider">Rotate By Custom Degrees</span>
<div class="flex gap-2 items-center">
<!-- Custom Stepper -->
<div class="flex items-center border border-gray-600 rounded-md bg-gray-800 overflow-hidden h-10">
<button id="rotate-all-decrement-btn" class="px-3 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-r border-gray-600 transition-colors flex items-center justify-center">
<i data-lucide="minus" class="w-4 h-4"></i>
</button>
<div class="relative w-20 h-full">
<input type="number" id="custom-rotate-all-input" placeholder="0" class="no-spinner w-full h-full bg-transparent text-white text-sm text-center focus:outline-none appearance-none m-0 p-0 border-none" min="-360" max="360">
</div>
<button id="rotate-all-increment-btn" class="px-3 h-full text-gray-400 hover:text-white hover:bg-gray-700 border-l border-gray-600 transition-colors flex items-center justify-center">
<i data-lucide="plus" class="w-4 h-4"></i>
</button>
</div>
<button id="rotate-all-custom-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-medium px-4 py-2 rounded-lg text-sm transition-colors shadow-sm h-10">
Apply
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -814,7 +947,7 @@ export const toolTemplates = {
flatten: () => ` flatten: () => `
<h2 class="text-2xl font-bold text-white mb-4">Flatten PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">Flatten PDF</h2>
<p class="mb-6 text-gray-400">Make PDF forms and annotations non-editable by flattening them.</p> <p class="mb-6 text-gray-400">Make PDF forms and annotations non-editable by flattening them.</p>
${createFileInputHTML()} ${createFileInputHTML({ multiple: true, showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="hidden mt-6 btn-gradient w-full">Flatten PDF</button> <button id="process-btn" class="hidden mt-6 btn-gradient w-full">Flatten PDF</button>
`, `,
@@ -897,6 +1030,7 @@ export const toolTemplates = {
<p class="mb-2 font-medium text-white">Total Pages: <span id="total-pages"></span></p> <p class="mb-2 font-medium text-white">Total Pages: <span id="total-pages"></span></p>
<label for="pages-to-delete" class="block mb-2 text-sm font-medium text-gray-300">Enter pages to delete (e.g., 2, 4-6, 9):</label> <label for="pages-to-delete" class="block mb-2 text-sm font-medium text-gray-300">Enter pages to delete (e.g., 2, 4-6, 9):</label>
<input type="text" id="pages-to-delete" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 mb-6" placeholder="e.g., 2, 4-6, 9"> <input type="text" id="pages-to-delete" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 mb-6" placeholder="e.g., 2, 4-6, 9">
<div id="delete-pages-preview" class="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-4 my-6"></div>
<button id="process-btn" class="btn-gradient w-full">Delete Pages & Download</button> <button id="process-btn" class="btn-gradient w-full">Delete Pages & Download</button>
</div> </div>
`, `,
+3 -3
View File
@@ -9,7 +9,7 @@ export interface RenderConfig {
batchSize?: number; batchSize?: number;
useLazyLoading?: boolean; useLazyLoading?: boolean;
lazyLoadMargin?: string; lazyLoadMargin?: string;
eagerLoadBatches?: number; // Number of batches to load ahead eagerly (default: 1) eagerLoadBatches?: number; // Number of batches to load ahead eagerly (default: 2)
onProgress?: (current: number, total: number) => void; onProgress?: (current: number, total: number) => void;
onPageRendered?: (pageIndex: number, element: HTMLElement) => void; onPageRendered?: (pageIndex: number, element: HTMLElement) => void;
onBatchComplete?: () => void; onBatchComplete?: () => void;
@@ -214,7 +214,7 @@ export async function renderPagesProgressively(
const { const {
batchSize = 8, // Increased from 5 to 8 for faster initial render batchSize = 8, // Increased from 5 to 8 for faster initial render
useLazyLoading = true, useLazyLoading = true,
eagerLoadBatches = 1, // Eagerly load 1 batch ahead by default eagerLoadBatches = 2, // Eagerly load 1 batch ahead by default
onProgress, onProgress,
onBatchComplete, onBatchComplete,
} = config; } = config;
@@ -318,7 +318,7 @@ export function observePlaceholder(
* Eagerly renders the next batch in the background * Eagerly renders the next batch in the background
*/ */
function renderEagerBatch(config: RenderConfig): void { function renderEagerBatch(config: RenderConfig): void {
const { eagerLoadBatches = 1, batchSize = 8 } = config; const { eagerLoadBatches = 2, batchSize = 8 } = config;
if (eagerLoadBatches <= 0 || lazyLoadState.eagerLoadQueue.length === 0) { if (eagerLoadBatches <= 0 || lazyLoadState.eagerLoadQueue.length === 0) {
return; return;
+27
View File
@@ -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">
&copy; 2025 BentoPDF. All rights reserved.
</p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version-simple">${APP_VERSION}</span>
</p>
</div>
`;
document.body.appendChild(simpleFooter);
}
}
+10 -3
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Add Stamps - BentoPDF</title> <title>Add Stamps - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
</head> </head>
@@ -105,7 +108,7 @@
</div> </div>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]"> <footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"> <div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0"> <div class="mb-8 md:mb-0">
@@ -139,6 +142,9 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <li>
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a> <a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
</li> </li>
@@ -163,7 +169,7 @@
class="text-gray-400 hover:text-indigo-400" title="Discord"> class="text-gray-400 hover:text-indigo-400" title="Discord">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"> <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path <path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.19 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" /> d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
</svg> </svg>
</a> </a>
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400" <a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
@@ -187,6 +193,7 @@
</footer> </footer>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script> <script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../version.ts"></script> <script type="module" src="../version.ts"></script>
<script type="module" src="../js/logic/add-stamps.ts"></script> <script type="module" src="../js/logic/add-stamps.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
+20 -10
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Advanced PDF Bookmark Tool - BentoPDF</title> <title>Advanced PDF Bookmark Tool - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
<link href="../../src/css/bookmark.css" rel="stylesheet" /> <link href="../../src/css/bookmark.css" rel="stylesheet" />
</head> </head>
@@ -271,7 +274,7 @@
<input type="text" id="bookmark-title" placeholder="Bookmark title..." <input type="text" id="bookmark-title" placeholder="Bookmark title..."
class="w-full px-3 py-2 border border-gray-300 rounded-lg mb-2 text-sm" /> class="w-full px-3 py-2 border border-gray-300 rounded-lg mb-2 text-sm" />
<button id="add-top-level-btn" <button id="add-top-level-btn"
class="w-full px-3 py-2 bg-gradient-to-b from-blue-500 to-blue-600 hover:shadow-xl text-white rounded text-sm font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200"> class="w-full px-3 py-2 btn-gradient hover:shadow-xl text-white rounded text-sm font-medium !flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
<i data-lucide="plus" class="w-4 h-4"></i> <i data-lucide="plus" class="w-4 h-4"></i>
<span>Add to Page <span id="current-page-display">1</span></span> <span>Add to Page <span id="current-page-display">1</span></span>
</button> </button>
@@ -391,11 +394,14 @@
<div class="space-y-2"> <div class="space-y-2">
<button id="download-btn" <button id="download-btn"
class="w-full px-4 py-2 bg-gradient-to-b from-blue-600 to-blue-700 hover:shadow-xl text-white rounded font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200"> class="w-full px-4 py-2 btn-gradient hover:shadow-xl text-white rounded font-medium !flex items-center justify-center gap-2 focus:ring-2 focus:ring-blue-400 transition duration-200">
Save PDF with Bookmarks Save PDF with Bookmarks
</button> </button>
<button id="extract-existing-btn" <button id="extract-existing-btn"
class="w-full px-4 py-2 bg-gradient-to-b from-green-500 to-green-600 hover:shadow-xl text-white rounded font-medium flex items-center justify-center gap-2 focus:ring-2 focus:ring-green-400 transition duration-200"> class="w-full px-4 py-2 bg-gray-50 text-gray-800
border border-gray-200 rounded-xl font-medium
flex items-center justify-center gap-2
shadow-sm hover:shadow-md transition duration-200">
Extract Existing Bookmarks Extract Existing Bookmarks
</button> </button>
</div> </div>
@@ -415,7 +421,7 @@
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"> <div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0"> <div class="mb-8 md:mb-0">
<div class="flex items-center justify-center md:justify-start mb-4"> <div class="flex items-center justify-center md:justify-start mb-4">
<img src="/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" /> <img src="../../public/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
<span class="text-xl font-bold text-white">BentoPDF</span> <span class="text-xl font-bold text-white">BentoPDF</span>
</div> </div>
<p class="text-gray-400 text-sm"> <p class="text-gray-400 text-sm">
@@ -430,13 +436,13 @@
<h3 class="font-bold text-white mb-4">Company</h3> <h3 class="font-bold text-white mb-4">Company</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="./about.html" class="hover:text-indigo-400">About Us</a> <a href="../../about.html" class="hover:text-indigo-400">About Us</a>
</li> </li>
<li> <li>
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a> <a href="../../faq.html" class="hover:text-indigo-400">FAQ</a>
</li> </li>
<li> <li>
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a> <a href="../../contact.html" class="hover:text-indigo-400">Contact Us</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -445,10 +451,13 @@
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li> <li>
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a> <a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li> </li>
<li> <li>
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a> <a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
</li>
<li>
<a href="../../privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -492,6 +501,7 @@
</footer> </footer>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script> <script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../version.ts"></script> <script type="module" src="../version.ts"></script>
<script type="module" src="../js/logic/bookmark-pdf.ts"></script> <script type="module" src="../js/logic/bookmark-pdf.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
+27 -20
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Create PDF Form - BentoPDF</title> <title>Create PDF Form - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
</head> </head>
@@ -298,7 +301,7 @@
</div> </div>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]"> <footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"> <div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0"> <div class="mb-8 md:mb-0">
@@ -332,6 +335,9 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <li>
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a> <a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
</li> </li>
@@ -378,27 +384,28 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Custom Modal --> </footer>
<div id="errorModal" <!-- Custom Modal -->
class="hidden fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-75"> <div id="errorModal" class="hidden fixed inset-0 z-[9999] flex items-center justify-center bg-black bg-opacity-75">
<div class="bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 border border-gray-700"> <div class="bg-gray-800 rounded-lg shadow-2xl max-w-md w-full mx-4 border border-gray-700">
<div class="p-6"> <div class="p-6">
<h3 id="errorModalTitle" class="text-xl font-bold text-white mb-3">Error</h3> <h3 id="errorModalTitle" class="text-xl font-bold text-white mb-3">Error</h3>
<p id="errorModalMessage" class="text-gray-300 mb-6">An error occurred.</p> <p id="errorModalMessage" class="text-gray-300 mb-6">An error occurred.</p>
<button id="errorModalClose" <button id="errorModalClose"
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded transition-colors"> class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2 px-4 rounded transition-colors">
Close Close
</button> </button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script> <script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../version.ts"></script> <script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../js/logic/form-creator.ts"></script> <script type="module" src="../version.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/logic/form-creator.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+9 -2
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JSON to PDF Converter - BentoPDF</title> <title>JSON to PDF Converter - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
</head> </head>
@@ -105,7 +108,7 @@
</div> </div>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]"> <footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"> <div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0"> <div class="mb-8 md:mb-0">
@@ -139,6 +142,9 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <li>
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a> <a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
</li> </li>
@@ -188,6 +194,7 @@
<script type="module" src="../js/logic/json-to-pdf.ts"></script> <script type="module" src="../js/logic/json-to-pdf.ts"></script>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script> <script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../version.ts"></script> <script type="module" src="../version.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
+4 -1
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PDF Multi Tool - BentoPDF</title> <title>PDF Multi Tool - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
<style> <style>
+9 -2
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PDF to JSON Converter - BentoPDF</title> <title>PDF to JSON Converter - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
</head> </head>
@@ -99,7 +102,7 @@
</div> </div>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]"> <footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"> <div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0"> <div class="mb-8 md:mb-0">
@@ -133,6 +136,9 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <li>
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a> <a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
</li> </li>
@@ -182,6 +188,7 @@
<script type="module" src="../js/logic/pdf-to-json.ts"></script> <script type="module" src="../js/logic/pdf-to-json.ts"></script>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script> <script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../version.ts"></script> <script type="module" src="../version.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
+221
View File
@@ -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">
&copy; 2025 BentoPDF. All rights reserved.
</p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span>
</p>
</div>
<div>
<h3 class="font-bold text-white mb-4">Company</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="../../about.html" class="hover:text-indigo-400">About Us</a>
</li>
<li>
<a href="../../faq.html" class="hover:text-indigo-400">FAQ</a>
</li>
<li>
<a href="../../contact.html" class="hover:text-indigo-400">Contact Us</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li>
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
</li>
<li>
<a href="../../privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Follow Us</h3>
<div class="flex justify-center md:justify-start space-x-4">
<a href="https://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="GitHub">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
</svg>
</a>
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"
title="Instagram">
<i data-lucide="instagram"></i>
</a>
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"
title="LinkedIn">
<i data-lucide="linkedin"></i>
</a>
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
</a>
</div>
</div>
</div>
</div>
</footer>
<script type="module" src="../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>
+12 -2
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Generate Table of Contents - BentoPDF</title> <title>Generate Table of Contents - BentoPDF</title>
<link rel="icon" type="image/png" href="../../images/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link href="../../src/css/styles.css" rel="stylesheet" /> <link href="../../src/css/styles.css" rel="stylesheet" />
</head> </head>
@@ -171,7 +174,7 @@
</div> </div>
</div> </div>
<footer class="mt-16 border-t-2 border-gray-700 py-8 bg-[#111827]"> <footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"> <div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0"> <div class="mb-8 md:mb-0">
@@ -182,6 +185,9 @@
<p class="text-gray-400 text-sm"> <p class="text-gray-400 text-sm">
&copy; 2025 BentoPDF. All rights reserved. &copy; 2025 BentoPDF. All rights reserved.
</p> </p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span>
</p>
</div> </div>
<div> <div>
@@ -202,6 +208,9 @@
<div> <div>
<h3 class="font-bold text-white mb-4">Legal</h3> <h3 class="font-bold text-white mb-4">Legal</h3>
<ul class="space-y-2 text-gray-400"> <ul class="space-y-2 text-gray-400">
<li>
<a href="../../licensing.html" class="hover:text-indigo-400">Licensing</a>
</li>
<li> <li>
<a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a> <a href="../../terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
</li> </li>
@@ -250,6 +259,7 @@
</footer> </footer>
<script type="module" src="../js/utils/lucide-init.ts"></script> <script type="module" src="../js/utils/lucide-init.ts"></script>
<script type="module" src="../js/utils/full-width.ts"></script> <script type="module" src="../js/utils/full-width.ts"></script>
<script type="module" src="../js/utils/simple-mode-footer.ts"></script>
<script type="module" src="../js/logic/table-of-contents.ts"></script> <script type="module" src="../js/logic/table-of-contents.ts"></script>
<script type="module" src="../js/mobileMenu.ts"></script> <script type="module" src="../js/mobileMenu.ts"></script>
</body> </body>
+4 -1
View File
@@ -5,7 +5,10 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Terms and Conditions - Bentopdf</title> <title>Terms and Conditions - Bentopdf</title>
<link rel="icon" type="image/png" href="images/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/images/favicon.svg" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link rel="apple-touch-icon" href="/images/favicon.png" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link href="/src/css/styles.css" rel="stylesheet" /> <link href="/src/css/styles.css" rel="stylesheet" />
</head> </head>
+1
View File
@@ -61,6 +61,7 @@ export default defineConfig(({ mode }) => ({
'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'), 'pdf-multi-tool': resolve(__dirname, 'src/pages/pdf-multi-tool.html'),
'add-stamps': resolve(__dirname, 'src/pages/add-stamps.html'), 'add-stamps': resolve(__dirname, 'src/pages/add-stamps.html'),
'form-creator': resolve(__dirname, 'src/pages/form-creator.html'), 'form-creator': resolve(__dirname, 'src/pages/form-creator.html'),
'repair-pdf': resolve(__dirname, 'src/pages/repair-pdf.html'),
}, },
}, },
}, },