Compare commits

..
25 Commits
Author SHA1 Message Date
abdullahalam123 6ad6a7d730 Release v1.0.0 2025-10-20 18:52:28 +05:30
abdullahalam123 0e7c53560a feat(ocr): add whitelist presets and improve UI for OCR tool
refactor: format code and improve mobile menu accessibility
style: fix whitespace and formatting in multiple files
docs: update documentation with better formatting and examples
2025-10-20 18:51:49 +05:30
abdullahalam123 59b351eee4 Release v0.0.1 2025-10-20 18:32:20 +05:30
Alam 48fe49ac37 Merge pull request #64 from AltuisticIsopod/main
feat: add Simple Mode for internal organizational use
2025-10-20 18:22:31 +05:30
Alam ccb1b119c5 Merge pull request #58 from divy-11/feature/reverse-multi-pdfs
Feature/reverse multi pdfs
2025-10-20 18:14:58 +05:30
Alam 6971c18e5e Merge pull request #65 from lunas-awful-coding-ideas/main
added description to Unraid XML
2025-10-20 18:09:01 +05:30
lunas-coding 8a72063ef7 added description to Unraid XML
added port name to the unraid XML so users know directly what the port is being used for
2025-10-20 13:30:48 +02:00
divy-11 4e6cc77a28 Removed new state pdfDocs
- update reverse-pages function
- updated test-case
2025-10-20 16:39:45 +05:30
Lalit Sudhir f037e27535 feat: add Simple Mode for internal organizational use 2025-10-20 01:42:50 -07:00
Alam 6270080045 Merge pull request #62 from yasirakhlaque/ImprovedNavbarUI
Added hamburger Navbar
2025-10-20 11:24:47 +05:30
Yasir Akhlaque f9a06b54f5 Aligned text centered 2025-10-20 11:12:26 +05:30
Yasir Akhlaque 584f2f09d1 Added hamburger navbar 2025-10-20 10:54:17 +05:30
Alam 8ac4793f9b Merge pull request #52 from AltuisticIsopod/main
feat(versioning): Added version management sysytem
2025-10-20 10:46:26 +05:30
Alam d163bd29e6 Merge pull request #60 from ShirshenduR/Fix#20
Fix#20 : fixed the tesseract issue
2025-10-20 10:32:18 +05:30
Alam 34a00ad74a Merge pull request #49 from Utkarsh-0304/feature/searchbar_shortcut
feat: add search bar keyboard shortcut
2025-10-19 22:29:11 +05:30
Shirshendu R Tripathi 797d8f5b9d Fix#20 OCR pdf text not selectable 2025-10-19 22:13:49 +05:30
Shirshendu R Tripathi ef7b9d0365 Fix#20 OCR pdf text not selectable 2025-10-19 22:12:51 +05:30
Utkarsh Agarwal 6bfcb30592 fix caps on focus and ios/android disabled display 2025-10-19 20:22:45 +05:30
divy-11 a605e56834 test-case added. 2025-10-19 19:50:19 +05:30
divy-11 2e2c634b41 Feat: Reverse-pages for mulit-pdf
- Added new state pdfDocs: [ ]
- Made tweaks for new state in fileHandler.ts
2025-10-19 18:36:57 +05:30
Alam caf23b0ab9 Merge pull request #53 from alam00000/add-attachments
feat(pdf-tools): add attachments feature to embed files in PDFs
2025-10-19 15:06:39 +05:30
abdullahalam123 88e05e6713 feat(pdf-tools): add attachments feature to embed files in PDFs
Implement new functionality to allow embedding attachments into PDF documents. The feature includes:
- UI for selecting PDF and files to attach
- Logic to embed files while preserving metadata
- Display of attached files with size information
- Download of modified PDF with embedded files
2025-10-19 15:02:27 +05:30
Lalit Sudhir b3ab445538 Added version management sysytem 2025-10-19 02:03:04 -07:00
Alam ddeebc8142 Merge pull request #51 from alam00000/linearize
feat(pdf-tools): add linearize pdf tool for web optimization
2025-10-19 14:03:15 +05:30
Utkarsh Agarwal f11efe7278 Add Search bar Keyboard Shortcut 2025-10-18 23:58:41 +05:30
32 changed files with 1627 additions and 128 deletions
+21 -1
View File
@@ -4,10 +4,15 @@ on:
push: push:
branches: branches:
- 'main' - 'main'
tags:
- 'v*'
jobs: jobs:
docker-build-and-push: docker-build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps: steps:
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
@@ -18,9 +23,24 @@ jobs:
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }} password: ${{ secrets.DOCKER_TOKEN }}
- name: Extract version from tag
id: version
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_without_v=${VERSION#v}" >> $GITHUB_OUTPUT
else
echo "version=latest" >> $GITHUB_OUTPUT
echo "version_without_v=latest" >> $GITHUB_OUTPUT
fi
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
push: true push: true
tags: bentopdf/bentopdf:latest tags: |
bentopdf/bentopdf:latest
bentopdf/bentopdf:${{ steps.version.outputs.version_without_v }}
bentopdf/bentopdf:${{ steps.version.outputs.version }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
+3
View File
@@ -9,6 +9,9 @@ RUN npm ci
COPY . . COPY . .
# Build without type checking (vite build only) # Build without type checking (vite build only)
# Pass SIMPLE_MODE environment variable if provided
ARG SIMPLE_MODE=false
ENV SIMPLE_MODE=$SIMPLE_MODE
RUN npm run build -- --mode production RUN npm run build -- --mode production
# Production stage # Production stage
+38 -1
View File
@@ -1,4 +1,5 @@
# BentoPDF # BentoPDF
**BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that allows you to manipulate, edit, merge, and process PDF files directly in your browser. No server-side processing is required, ensuring your files remain secure and private. **BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that allows you to manipulate, edit, merge, and process PDF files directly in your browser. No server-side processing is required, ensuring your files remain secure and private.
![Docker Pulls](https://img.shields.io/docker/pulls/bentopdf/bentopdf) [![Ko-fi](https://img.shields.io/badge/Buy%20me%20a%20Coffee-yellow?logo=kofi&style=flat-square)](https://ko-fi.com/alio0) ![GitHub Stars](https://img.shields.io/github/stars/alam00000/bentopdf?style=social) ![Docker Pulls](https://img.shields.io/docker/pulls/bentopdf/bentopdf) [![Ko-fi](https://img.shields.io/badge/Buy%20me%20a%20Coffee-yellow?logo=kofi&style=flat-square)](https://ko-fi.com/alio0) ![GitHub Stars](https://img.shields.io/github/stars/alam00000/bentopdf?style=social)
@@ -129,6 +130,42 @@ docker-compose up -d
The application will be available at `http://localhost:3000`. The application will be available at `http://localhost:3000`.
### 🏢 Simple Mode for Internal Use
For organizations that want a clean, distraction-free interface focused solely on PDF tools, BentoPDF supports a **Simple Mode** that hides all branding and marketing content.
**What Simple Mode does:**
- Hides navigation, hero section, features, FAQ, testimonials, and footer
- Shows only the essential PDF tools
- Updates page title to "PDF Tools"
- Perfect for internal company tools and educational institutions
For more details, see [SIMPLE_MODE.md](SIMPLE_MODE.md).
### 📦 Version Management
BentoPDF supports semantic versioning with multiple Docker tags:
- **Latest**: `bentopdf/bentopdf:latest`
- **Specific Version**: `bentopdf/bentopdf:1.0.0`
- **Version with Prefix**: `bentopdf/bentopdf:v1.0.0`
#### Quick Release
```bash
# Release a patch version (0.0.1 → 0.0.2)
npm run release
# Release a minor version (0.0.1 → 0.1.0)
npm run release:minor
# Release a major version (0.0.1 → 1.0.0)
npm run release:major
```
````
For detailed release instructions, see [RELEASE.md](RELEASE.md).
### 🚀 Development Setup ### 🚀 Development Setup
#### Option 1: Run with npm #### Option 1: Run with npm
@@ -138,7 +175,7 @@ The application will be available at `http://localhost:3000`.
```bash ```bash
git clone https://github.com/alam00000/bentopdf.git git clone https://github.com/alam00000/bentopdf.git
cd bentopdf cd bentopdf
``` ````
2. **Install Dependencies**: 2. **Install Dependencies**:
+317
View File
@@ -0,0 +1,317 @@
# 🚀 Release Process - Real-World Scenarios
## 📋 Common Release Scenarios
### **Scenario 1: I Just Finished a Feature and Want to Release**
**Situation:** You've completed a new feature, tested it locally, and want to release it.
**Current State:**
```bash
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
modified: src/js/new-feature.js
modified: src/css/styles.css
modified: README.md
Untracked files:
src/js/feature-helper.js
```
**Steps:**
```bash
# 1. Commit your feature changes
git add .
git commit -m "Add new PDF watermark feature"
# 2. Choose your release type and run
npm run release # Patch: 1.0.0 → 1.0.1 (bug fixes, small improvements)
npm run release:minor # Minor: 1.0.0 → 1.1.0 (new features, backward compatible)
npm run release:major # Major: 1.0.0 → 2.0.0 (breaking changes)
```
**What Happens:**
- ✅ Your feature commit stays as-is
- ✅ Version gets bumped in `package.json`
- ✅ New release commit is created
- ✅ Git tag is created (e.g., `v1.0.1`)
- ✅ Everything gets pushed to GitHub
- ✅ Docker image gets built and published
---
### **Scenario 2: I Have Uncommitted Changes and Want to Release**
**Situation:** You have local changes but haven't committed them yet.
**Current State:**
```bash
$ git status
Changes not staged for commit:
modified: package.json
modified: src/js/main.js
modified: README.md
```
**❌ This Will Fail:**
```bash
npm run release
# Error: Your local changes would be overwritten by merge
```
**✅ Solution Options:**
**Option A: Commit Everything First (Recommended)**
```bash
git add .
git commit -m "Add new features and improvements"
npm run release
```
**Option B: Stash Changes Temporarily**
```bash
git stash
npm run release
git stash pop # Restore your changes after release
```
**Option C: Commit Only What's Needed**
```bash
git add package.json src/js/main.js
git commit -m "Add core improvements"
npm run release
git add README.md
git commit -m "Update documentation"
```
---
### **Scenario 3: I Want to Release a Hotfix**
**Situation:** There's a critical bug in production that needs immediate fixing.
**Steps:**
```bash
# 1. Fix the bug
git add src/js/bug-fix.js
git commit -m "Fix critical PDF rendering issue"
# 2. Release as patch (bug fix)
npm run release
# This creates: 1.0.0 → 1.0.1
```
**Result:**
- ✅ Bug fix gets released immediately
- ✅ Docker image with fix is available
- ✅ Users can pull the fixed version
---
### **Scenario 4: I Want to Release a Major Update**
**Situation:** You've added significant new features that might break existing functionality.
**Steps:**
```bash
# 1. Commit all your changes
git add .
git commit -m "Add major PDF editing features and API changes"
# 2. Release as major version
npm run release:major
# This creates: 1.0.0 → 2.0.0
```
**Result:**
- ✅ Major version bump indicates breaking changes
- ✅ Users know to check compatibility
- ✅ Both old and new versions available
---
### **Scenario 5: I Want to Release Multiple Features at Once**
**Situation:** You've been working on multiple features and want to release them together.
**Steps:**
```bash
# 1. Commit all features
git add .
git commit -m "Add multiple PDF tools: watermark, encryption, and compression"
# 2. Choose appropriate release type
npm run release:minor # For new features (1.0.0 → 1.1.0)
# OR
npm run release:major # For breaking changes (1.0.0 → 2.0.0)
```
---
### **Scenario 6: I Want to Test the Release Process**
**Situation:** You want to test the release system without affecting production.
**Steps:**
```bash
# 1. Make a small test change
echo "// Test comment" >> src/js/main.js
git add src/js/main.js
git commit -m "Test release process"
# 2. Run patch release
npm run release
# This creates: 1.0.0 → 1.0.1
# 3. Verify everything works
# Check GitHub Actions, Docker Hub, etc.
# 4. If you want to undo the test release
git tag -d v1.0.1
git push origin :refs/tags/v1.0.1
git reset --hard HEAD~1
```
---
## 🎯 **Release Type Guidelines**
| Scenario | Command | Version Change | When to Use |
| ------------------- | ----------------------- | --------------- | ------------------------------------ |
| **Bug Fix** | `npm run release` | `1.0.0 → 1.0.1` | Fixing bugs, small improvements |
| **New Feature** | `npm run release:minor` | `1.0.0 → 1.1.0` | Adding features, backward compatible |
| **Breaking Change** | `npm run release:major` | `1.0.0 → 2.0.0` | API changes, major rewrites |
---
## 🔄 **What Happens After You Run a Release Command**
### **Immediate Actions (Local):**
1. **Version Update**: `package.json` version gets bumped
2. **Git Commit**: New commit created with "Release vX.X.X"
3. **Git Tag**: Tag created (e.g., `v1.0.1`)
4. **Git Push**: Everything pushed to GitHub
### **Automatic Actions (GitHub):**
1. **GitHub Actions Triggered**: Workflow starts building Docker image
2. **Docker Build**: Multi-architecture image created
3. **Docker Push**: Images pushed to Docker Hub with tags:
- `bentopdf/bentopdf:latest`
- `bentopdf/bentopdf:1.0.1`
- `bentopdf/bentopdf:v1.0.1`
### **End Result:**
Users can immediately pull your new version:
```bash
docker pull bentopdf/bentopdf:1.0.1
```
---
## 🚨 **Before You Release - Prerequisites**
### **1. Docker Hub Credentials Setup**
You need to add these secrets to your GitHub repository:
1. Go to **Settings** → **Secrets and variables** → **Actions**
2. Add these secrets:
- `DOCKER_USERNAME`: Your Docker Hub username
- `DOCKER_TOKEN`: Your Docker Hub access token
### **2. Get Docker Hub Token**
1. Go to [Docker Hub](https://hub.docker.com)
2. Account Settings → Security → New Access Token
3. Set permissions to "Read, Write, Delete"
4. Copy the token and add it to GitHub Secrets
---
## 🔧 **Troubleshooting Common Issues**
### **❌ "Your local changes would be overwritten by merge"**
**Problem:** You have uncommitted changes
**Solution:**
```bash
git add .
git commit -m "Your commit message"
npm run release
```
### **❌ "Permission denied" in GitHub Actions**
**Problem:** Missing Docker Hub credentials
**Solution:** Add `DOCKER_USERNAME` and `DOCKER_TOKEN` to GitHub Secrets
### **❌ "Tag already exists"**
**Problem:** You've run the same release before
**Solution:** This is normal! The script will skip creating duplicate tags
### **❌ GitHub Actions fails**
**Problem:** Various build issues
**Solution:**
1. Check Actions tab for detailed logs
2. Verify Docker Hub credentials
3. Check Dockerfile for syntax errors
---
## 🧪 **Testing Your Release System**
### **Quick Test:**
```bash
# Make a small change
echo "// Test" >> src/js/main.js
git add src/js/main.js
git commit -m "Test release"
npm run release
```
### **Verify Results:**
1. **GitHub Actions**: Check Actions tab for successful build
2. **Docker Hub**: Verify images are published
3. **Git Tags**: `git tag --list` should show new tag
4. **Version**: `cat package.json | grep version` should show updated version
### **Undo Test Release:**
```bash
git tag -d v1.0.1
git push origin :refs/tags/v1.0.1
git reset --hard HEAD~1
```
---
## 🎉 **That's It!**
Your release system is now ready! Just follow the scenarios above based on your situation and run the appropriate `npm run release` command.
+91
View File
@@ -0,0 +1,91 @@
# Simple Mode for BentoPDF
Simple Mode is designed for internal organizational use where you want to hide all branding and marketing content, showing only the essential PDF tools for your users.
## What Simple Mode Does
When enabled, Simple Mode will:
- Hide the navigation bar
- Hide the hero section with marketing content
- Hide the features section
- Hide the security/compliance section
- Hide the FAQ section
- Hide the testimonials section
- Hide the support section
- Hide the footer
- Update the page title to "PDF Tools"
- Make the tools section more prominent
## How to Enable Simple Mode
### Using Docker Compose
1. Create a `.env` file in your project root:
```bash
SIMPLE_MODE=true
```
2. Run with docker-compose:
```bash
docker-compose up -d
```
### Using Docker Build
Build the image with the SIMPLE_MODE environment variable:
```bash
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
```
### Using Environment Variables
Set the environment variable before building:
```bash
export SIMPLE_MODE=true
npm run build
```
## 🧪 Testing Simple Mode Locally
After building with Simple Mode enabled, you need to serve the built files locally.
```bash
# Build with simple mode
SIMPLE_MODE=true npm run build
# Serve the built files
npx serve dist -p 3000
```
Then open `http://localhost:3000` in your browser.
## 🔍 What to Look For
When Simple Mode is working correctly, you should see:
- ✅ Clean "PDF Tools" header (no marketing hero section)
- ✅ "Select a tool to get started" subtitle
- ✅ Search bar for tools
- ✅ All PDF tool cards organized by category
- ❌ No navigation bar
- ❌ No hero section with "The PDF Toolkit built for privacy"
- ❌ No features, FAQ, testimonials, or footer sections
## Example Docker Compose Configuration
```yaml
services:
bentopdf:
image: bentopdf/bentopdf:latest
container_name: bentopdf
restart: unless-stopped
ports:
- '3000:80'
environment:
- SIMPLE_MODE=true
```
+70 -9
View File
@@ -15,18 +15,21 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<a <div
href="index.html"
class="flex-shrink-0 flex items-center cursor-pointer" class="flex-shrink-0 flex items-center cursor-pointer"
id="home-logo" id="home-logo"
> >
<img <img
src="images/favicon.svg" src="images/favicon.svg"
alt="Bentopdf Logo" alt="Bento PDF Logo"
class="h-8 w-8 mr-2" class="h-8 w-8"
/> />
<span class="text-white font-bold text-xl">BentoPDF</span> <span class="text-white font-bold text-xl ml-2">
</a> <a href="index.html">BentoPDF</a>
</span>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8"> <div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
@@ -34,12 +37,69 @@
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<div class="md:hidden flex items-center gap-4"> <!-- Mobile Hamburger Button -->
<a href="index.html" class="nav-link">Home</a> <div class="md:hidden flex items-center">
<a href="index.html#tools-header" class="nav-link">All Tools</a> <button
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<svg
id="menu-icon"
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<!-- Close Icon -->
<svg
id="close-icon"
class="hidden h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Mobile Menu Dropdown -->
<div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="index.html#tools-header" class="mobile-nav-link"
>All Tools</a
>
</div>
</div>
</nav> </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">
@@ -286,5 +346,6 @@
<script> <script>
lucide.createIcons(); lucide.createIcons();
</script> </script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+70 -8
View File
@@ -13,17 +13,21 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<a <div
href="index.html"
class="flex-shrink-0 flex items-center cursor-pointer" class="flex-shrink-0 flex items-center cursor-pointer"
id="home-logo"
> >
<img <img
src="images/favicon.svg" src="images/favicon.svg"
alt="Bento PDF Logo" alt="Bento PDF Logo"
class="h-8 w-8 mr-2" class="h-8 w-8"
/> />
<span class="text-white font-bold text-xl"> BentoPDF</span> <span class="text-white font-bold text-xl ml-2">
</a> <a href="index.html">BentoPDF</a>
</span>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8"> <div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
@@ -31,12 +35,69 @@
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<div class="md:hidden flex items-center gap-4"> <!-- Mobile Hamburger Button -->
<a href="index.html" class="nav-link">Home</a> <div class="md:hidden flex items-center">
<a href="index.html#tools-header" class="nav-link">All Tools</a> <button
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<svg
id="menu-icon"
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<!-- Close Icon -->
<svg
id="close-icon"
class="hidden h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Mobile Menu Dropdown -->
<div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="index.html#tools-header" class="mobile-nav-link"
>All Tools</a
>
</div>
</div>
</nav> </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">
@@ -150,5 +211,6 @@
<script> <script>
lucide.createIcons(); lucide.createIcons();
</script> </script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+3 -1
View File
@@ -4,4 +4,6 @@ services:
container_name: bentopdf container_name: bentopdf
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:80" - '3000:80'
environment:
- SIMPLE_MODE=${SIMPLE_MODE:-false}
+69 -8
View File
@@ -16,18 +16,21 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<a <div
href="index.html"
class="flex-shrink-0 flex items-center cursor-pointer" class="flex-shrink-0 flex items-center cursor-pointer"
id="home-logo" id="home-logo"
> >
<img <img
src="images/favicon.svg" src="images/favicon.svg"
alt="Bento PDF Logo" alt="Bento PDF Logo"
class="h-8 w-8 mr-2" class="h-8 w-8"
/> />
<span class="text-white font-bold text-xl">BentoPDF</span> <span class="text-white font-bold text-xl ml-2">
</a> <a href="index.html">BentoPDF</a>
</span>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8"> <div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
@@ -35,12 +38,69 @@
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<div class="md:hidden flex items-center gap-4"> <!-- Mobile Hamburger Button -->
<a href="index.html" class="nav-link">Home</a> <div class="md:hidden flex items-center">
<a href="index.html#tools-header" class="nav-link">All Tools</a> <button
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<svg
id="menu-icon"
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<!-- Close Icon -->
<svg
id="close-icon"
class="hidden h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Mobile Menu Dropdown -->
<div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="index.html#tools-header" class="mobile-nav-link"
>All Tools</a
>
</div>
</div>
</nav> </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">
@@ -339,5 +399,6 @@
<script> <script>
lucide.createIcons(); lucide.createIcons();
</script> </script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+62 -11
View File
@@ -48,6 +48,7 @@
</span> </span>
</div> </div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8"> <div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
@@ -55,22 +56,66 @@
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<div class="md:hidden flex items-center gap-2"> <!-- Mobile Hamburger Button -->
<a href="index.html" class="nav-link">Home</a> <div class="md:hidden flex items-center">
<a href="index.html#tools-header" class="nav-link">All Tools</a> <button
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<svg
id="menu-icon"
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<!-- Close Icon -->
<svg
id="close-icon"
class="hidden h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>
<div id="mobile-menu" class="hidden md:hidden">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3"> <!-- Mobile Menu Dropdown -->
<div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a> <a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a> <a href="./contact.html" class="mobile-nav-link">Contact</a>
<hr class="border-gray-700 my-2" /> <a href="index.html#tools-header" class="mobile-nav-link"
<a >All Tools</a
href="#tool-grid"
id="mobile-all-tools-link"
class="mobile-nav-link bg-gray-700 text-white"
>View All Tools</a
> >
</div> </div>
</div> </div>
@@ -218,6 +263,11 @@
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"
>
<kbd id="shortcut" class="bg-gray-800 px-1 rounded-lg"></kbd>
</span>
</div> </div>
</div> </div>
@@ -888,5 +938,6 @@
</footer> </footer>
<script type="module" src="src/js/main.ts"></script> <script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+5 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "bento-pdf", "name": "bento-pdf",
"private": true, "private": true,
"version": "0.0.0", "version": "1.0.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -15,7 +15,10 @@
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
"test:watch": "vitest watch", "test:watch": "vitest watch",
"build:docker": "vite build", "build:docker": "vite build",
"format": "prettier --write ." "format": "prettier --write .",
"release": "node scripts/release.js patch",
"release:minor": "node scripts/release.js minor",
"release:major": "node scripts/release.js major"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
+71 -9
View File
@@ -15,17 +15,21 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<a <div
href="index.html"
class="flex-shrink-0 flex items-center cursor-pointer" class="flex-shrink-0 flex items-center cursor-pointer"
id="home-logo"
> >
<img <img
src="images/favicon.svg" src="images/favicon.svg"
alt="BentoPDF Logo" alt="Bento PDF Logo"
class="h-8 w-8 mr-2" class="h-8 w-8"
/> />
<span class="text-white font-bold text-xl">BentoPDF</span> <span class="text-white font-bold text-xl ml-2">
</a> <a href="index.html">BentoPDF</a>
</span>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8"> <div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
@@ -33,12 +37,69 @@
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<div class="md:hidden flex items-center gap-4"> <!-- Mobile Hamburger Button -->
<a href="index.html" class="nav-link">Home</a> <div class="md:hidden flex items-center">
<a href="index.html#tools-header" class="nav-link">All Tools</a> <button
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<svg
id="menu-icon"
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<!-- Close Icon -->
<svg
id="close-icon"
class="hidden h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Mobile Menu Dropdown -->
<div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="index.html#tools-header" class="mobile-nav-link"
>All Tools</a
>
</div>
</div>
</nav> </nav>
<div id="app" class="container mx-auto p-4 md:p-8"> <div id="app" class="container mx-auto p-4 md:p-8">
@@ -260,5 +321,6 @@
<script> <script>
lucide.createIcons(); lucide.createIcons();
</script> </script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env node
import fs from 'fs';
import path from 'path';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageJsonPath = path.join(__dirname, '../package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
function getCurrentVersion() {
return packageJson.version;
}
function updateVersion(type) {
const currentVersion = getCurrentVersion();
const [major, minor, patch] = currentVersion.split('.').map(Number);
let newVersion;
switch (type) {
case 'major':
newVersion = `${major + 1}.0.0`;
break;
case 'minor':
newVersion = `${major}.${minor + 1}.0`;
break;
case 'patch':
default:
newVersion = `${major}.${minor}.${patch + 1}`;
break;
}
packageJson.version = newVersion;
fs.writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, null, 2) + '\n'
);
return newVersion;
}
function createGitTag(version) {
const tagName = `v${version}`;
try {
// Check if tag already exists
execSync(`git rev-parse "v${version}" >/dev/null 2>&1`, {
stdio: 'ignore',
});
console.log(`✅ Tag v${version} already exists`);
return tagName;
} catch {
// Tag doesn't exist, create it
execSync(`git tag -a "v${version}" -m "Release v${version}"`, {
stdio: 'inherit',
});
console.log(`✅ Created tag v${version}`);
return tagName;
}
}
function main() {
const type = process.argv[2] || 'patch';
if (!['major', 'minor', 'patch'].includes(type)) {
console.error('❌ Invalid version type. Use: major, minor, or patch');
process.exit(1);
}
console.log(`🚀 Releasing ${type} version...`);
// 1. Update version in package.json
const newVersion = updateVersion(type);
console.log(`📦 Updated version to ${newVersion}`);
// 2. Add and commit changes
execSync('git add package.json', { stdio: 'inherit' });
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
console.log(`💾 Committed version change`);
// 3. Create git tag
const tagName = createGitTag(newVersion);
// 4. Push everything to main
console.log(`📤 Pushing to main...`);
execSync('git push origin main', { stdio: 'inherit' });
execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
console.log(`🎉 Release v${newVersion} complete!`);
console.log(`📦 Docker image: bentopdf/bentopdf:${newVersion}`);
console.log(
`🏷️ GitHub release: https://github.com/AltuisticIsopod/bentopdf/releases/tag/${tagName}`
);
}
main();
+2 -1
View File
@@ -18,7 +18,6 @@ export const singlePdfLoadTools = [
'add-header-footer', 'add-header-footer',
'invert-colors', 'invert-colors',
'view-metadata', 'view-metadata',
'reverse-pages',
'crop', 'crop',
'redact', 'redact',
'pdf-to-bmp', 'pdf-to-bmp',
@@ -38,6 +37,7 @@ export const singlePdfLoadTools = [
'form-filler', 'form-filler',
'posterize', 'posterize',
'remove-blank-pages', 'remove-blank-pages',
'add-attachments',
]; ];
export const simpleTools = [ export const simpleTools = [
@@ -61,4 +61,5 @@ export const multiFileTools = [
'tiff-to-pdf', 'tiff-to-pdf',
'alternate-merge', 'alternate-merge',
'linearize', 'linearize',
'reverse-pages',
]; ];
+7 -1
View File
@@ -282,6 +282,12 @@ export const categories = [
icon: 'files', icon: 'files',
subtitle: 'Duplicate, reorder, and delete pages.', subtitle: 'Duplicate, reorder, and delete pages.',
}, },
{
id: 'add-attachments',
name: 'Add Attachments',
icon: 'paperclip',
subtitle: 'Embed one or more files into your PDF.',
},
{ {
id: 'split', id: 'split',
name: 'Split PDF', name: 'Split PDF',
@@ -383,7 +389,7 @@ export const categories = [
icon: 'ruler-dimension-line', icon: 'ruler-dimension-line',
subtitle: 'Standardize all pages to a uniform size.', subtitle: 'Standardize all pages to a uniform size.',
}, },
{ {
id: 'linearize', id: 'linearize',
name: 'Linearize PDF', name: 'Linearize PDF',
icon: 'zap', icon: 'zap',
+5 -1
View File
@@ -324,7 +324,11 @@ async function handleSinglePdfUpload(toolId, file) {
} }
async function handleMultiFileUpload(toolId) { async function handleMultiFileUpload(toolId) {
if (toolId === 'merge' || toolId === 'alternate-merge') { if (
toolId === 'merge' ||
toolId === 'alternate-merge' ||
toolId === 'reverse-pages'
) {
const pdfFilesUnloaded: File[] = []; const pdfFilesUnloaded: File[] = [];
state.files.forEach((file) => { state.files.forEach((file) => {
+125
View File
@@ -0,0 +1,125 @@
import { showLoader, hideLoader, showAlert } from '../ui';
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
import { state } from '../state';
let attachments: File[] = [];
export async function addAttachments() {
if (!state.pdfDoc) {
showAlert('Error', 'Main PDF is not loaded.');
return;
}
if (attachments.length === 0) {
showAlert('No Files', 'Please select at least one file to attach.');
return;
}
showLoader('Embedding files into PDF...');
try {
const pdfDoc = state.pdfDoc;
for (let i = 0; i < attachments.length; i++) {
const file = attachments[i];
showLoader(`Attaching ${file.name} (${i + 1}/${attachments.length})...`);
const fileBytes = await readFileAsArrayBuffer(file);
await pdfDoc.attach(fileBytes as ArrayBuffer, file.name, {
mimeType: file.type || 'application/octet-stream',
description: `Attached file: ${file.name}`,
creationDate: new Date(),
modificationDate: new Date(file.lastModified),
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([pdfBytes], { type: 'application/pdf' }),
`attached-${state.files[0].name}`
);
showAlert(
'Success',
`${attachments.length} file(s) attached successfully.`
);
} catch (error: any) {
console.error('Error attaching files:', error);
showAlert('Error', `Failed to attach files: ${error.message}`);
} finally {
hideLoader();
clearAttachments();
}
}
function clearAttachments() {
attachments = [];
const fileListDiv = document.getElementById('attachment-file-list');
const attachmentInput = document.getElementById(
'attachment-files-input'
) as HTMLInputElement;
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
if (fileListDiv) fileListDiv.innerHTML = '';
if (attachmentInput) attachmentInput.value = '';
if (processBtn) {
processBtn.disabled = true;
processBtn.classList.add('hidden');
}
}
let isSetup = false; // Prevent duplicate setup
export function setupAddAttachmentsTool() {
if (isSetup) return; // Already set up
isSetup = true;
const optionsDiv = document.getElementById('attachment-options');
const attachmentInput = document.getElementById(
'attachment-files-input'
) as HTMLInputElement;
const fileListDiv = document.getElementById('attachment-file-list');
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
console.error('Attachment tool UI elements not found.');
return;
}
optionsDiv.classList.remove('hidden');
attachmentInput.addEventListener('change', (e) => {
const files = (e.target as HTMLInputElement).files;
if (files && files.length > 0) {
attachments = Array.from(files);
fileListDiv.innerHTML = '';
attachments.forEach((file) => {
const div = document.createElement('div');
div.className =
'flex justify-between items-center p-2 bg-gray-800 rounded-md text-white';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate text-sm';
nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'text-xs text-gray-400';
sizeSpan.textContent = `${Math.round(file.size / 1024)} KB`;
div.appendChild(nameSpan);
div.appendChild(sizeSpan);
fileListDiv.appendChild(div);
});
processBtn.disabled = false;
processBtn.classList.remove('hidden');
} else {
clearAttachments();
}
});
processBtn.onclick = addAttachments;
}
+5 -2
View File
@@ -8,7 +8,7 @@ export async function addBlankPage() {
const pageNumberInput = document.getElementById('page-number').value; const pageNumberInput = document.getElementById('page-number').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageCountInput = document.getElementById('page-count').value; const pageCountInput = document.getElementById('page-count').value;
if (pageNumberInput.trim() === '') { if (pageNumberInput.trim() === '') {
showAlert('Invalid Input', 'Please enter a page number.'); showAlert('Invalid Input', 'Please enter a page number.');
return; return;
@@ -31,7 +31,10 @@ export async function addBlankPage() {
} }
if (isNaN(pageCount) || pageCount < 1) { if (isNaN(pageCount) || pageCount < 1) {
showAlert('Invalid Input', 'Please enter a valid number of pages (1 or more).'); showAlert(
'Invalid Input',
'Please enter a valid number of pages (1 or more).'
);
return; return;
} }
+5
View File
@@ -62,6 +62,7 @@ import {
} from './remove-blank-pages.js'; } from './remove-blank-pages.js';
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js'; import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
import { linearizePdf } from './linearize.js'; import { linearizePdf } from './linearize.js';
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
export const toolLogic = { export const toolLogic = {
merge: { process: merge, setup: setupMergeTool }, merge: { process: merge, setup: setupMergeTool },
@@ -130,4 +131,8 @@ export const toolLogic = {
setup: setupAlternateMergeTool, setup: setupAlternateMergeTool,
}, },
linearize: linearizePdf, linearize: linearizePdf,
'add-attachments': {
process: addAttachments,
setup: setupAddAttachmentsTool,
},
}; };
+20 -19
View File
@@ -30,7 +30,9 @@ async function initializeQpdf() {
export async function linearizePdf() { export async function linearizePdf() {
// Check if there are files and at least one PDF // Check if there are files and at least one PDF
const pdfFiles = state.files.filter((file: File) => file.type === 'application/pdf'); const pdfFiles = state.files.filter(
(file: File) => file.type === 'application/pdf'
);
if (!pdfFiles || pdfFiles.length === 0) { if (!pdfFiles || pdfFiles.length === 0) {
showAlert('No PDF Files', 'Please upload at least one PDF file.'); showAlert('No PDF Files', 'Please upload at least one PDF file.');
return; return;
@@ -43,12 +45,12 @@ export async function linearizePdf() {
let errorCount = 0; let errorCount = 0;
try { try {
qpdf = await initializeQpdf(); qpdf = await initializeQpdf();
for (let i = 0; i < pdfFiles.length; i++) { for (let i = 0; i < pdfFiles.length; i++) {
const file = pdfFiles[i]; const file = pdfFiles[i];
const inputPath = `/input_${i}.pdf`; const inputPath = `/input_${i}.pdf`;
const outputPath = `/output_${i}.pdf`; const outputPath = `/output_${i}.pdf`;
showLoader(`Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`); showLoader(`Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`);
@@ -58,23 +60,20 @@ export async function linearizePdf() {
qpdf.FS.writeFile(inputPath, uint8Array); qpdf.FS.writeFile(inputPath, uint8Array);
const args = [ const args = [inputPath, '--linearize', outputPath];
inputPath,
'--linearize',
outputPath,
];
qpdf.callMain(args); qpdf.callMain(args);
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' }); const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (!outputFile || outputFile.length === 0) { if (!outputFile || outputFile.length === 0) {
console.error(`Linearization resulted in an empty file for ${file.name}.`); console.error(
throw new Error(`Processing failed for ${file.name}.`); `Linearization resulted in an empty file for ${file.name}.`
} );
throw new Error(`Processing failed for ${file.name}.`);
}
zip.file(`linearized-${file.name}`, outputFile, { binary: true }); zip.file(`linearized-${file.name}`, outputFile, { binary: true });
successCount++; successCount++;
} catch (fileError: any) { } catch (fileError: any) {
errorCount++; errorCount++;
console.error(`Failed to linearize ${file.name}:`, fileError); console.error(`Failed to linearize ${file.name}:`, fileError);
@@ -91,13 +90,16 @@ export async function linearizePdf() {
} }
} }
} catch (cleanupError) { } catch (cleanupError) {
console.warn(`Failed to cleanup WASM FS for ${file.name}:`, cleanupError); console.warn(
`Failed to cleanup WASM FS for ${file.name}:`,
cleanupError
);
} }
} }
} }
if (successCount === 0) { if (successCount === 0) {
throw new Error('No PDF files could be linearized.'); throw new Error('No PDF files could be linearized.');
} }
showLoader('Generating ZIP file...'); showLoader('Generating ZIP file...');
@@ -109,7 +111,6 @@ export async function linearizePdf() {
alertMessage += ` ${errorCount} file(s) failed.`; alertMessage += ` ${errorCount} file(s) failed.`;
} }
showAlert('Processing Complete', alertMessage); showAlert('Processing Complete', alertMessage);
} catch (error: any) { } catch (error: any) {
console.error('Linearization process error:', error); console.error('Linearization process error:', error);
showAlert( showAlert(
@@ -119,4 +120,4 @@ export async function linearizePdf() {
} finally { } finally {
hideLoader(); hideLoader();
} }
} }
+54 -15
View File
@@ -113,11 +113,9 @@ async function runOCR() {
tessjs_create_hocr: '1', tessjs_create_hocr: '1',
}); });
if (whitelist.trim()) { await worker.setParameters({
await worker.setParameters({ tessedit_char_whitelist: whitelist,
tessedit_char_whitelist: whitelist.trim(), });
});
}
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'. // @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument( const pdf = await pdfjsLib.getDocument(
@@ -144,7 +142,11 @@ async function runOCR() {
binarizeCanvas(context); binarizeCanvas(context);
} }
const result = await worker.recognize(canvas); const result = await worker.recognize(
canvas,
{},
{ text: true, hocr: true }
);
const data = result.data; const data = result.data;
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]); const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
const pngImageBytes = await new Promise((resolve) => const pngImageBytes = await new Promise((resolve) =>
@@ -281,9 +283,51 @@ export function setupOcrTool() {
); );
const processBtn = document.getElementById('process-btn'); const processBtn = document.getElementById('process-btn');
// Whitelist presets
const whitelistPresets: Record<string, string> = {
alphanumeric:
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"',
'numbers-currency': '0123456789$€£¥.,- ',
'letters-only': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ',
'numbers-only': '0123456789',
invoice: '0123456789$.,/-#: ',
forms:
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,()-_/@#:',
};
// Handle whitelist preset selection
const presetSelect = document.getElementById(
'whitelist-preset'
) as HTMLSelectElement;
const whitelistInput = document.getElementById(
'ocr-whitelist'
) as HTMLInputElement;
presetSelect?.addEventListener('change', (e) => {
const preset = (e.target as HTMLSelectElement).value;
if (preset && preset !== 'custom') {
whitelistInput.value = whitelistPresets[preset];
whitelistInput.disabled = true;
} else {
whitelistInput.disabled = false;
if (preset === '') {
whitelistInput.value = '';
}
}
});
// Handle details toggle icon rotation
document.querySelectorAll('details').forEach((details) => {
details.addEventListener('toggle', () => {
const icon = details.querySelector('.details-icon') as HTMLElement;
if (icon) {
icon.style.transform = details.open ? 'rotate(180deg)' : 'rotate(0deg)';
}
});
});
langSearch.addEventListener('input', () => { langSearch.addEventListener('input', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message const searchTerm = (langSearch as HTMLInputElement).value.toLowerCase();
const searchTerm = langSearch.value.toLowerCase();
langList.querySelectorAll('label').forEach((label) => { langList.querySelectorAll('label').forEach((label) => {
label.style.display = label.textContent.toLowerCase().includes(searchTerm) label.style.display = label.textContent.toLowerCase().includes(searchTerm)
? '' ? ''
@@ -291,19 +335,14 @@ export function setupOcrTool() {
}); });
}); });
// Update the display of selected languages
langList.addEventListener('change', () => { langList.addEventListener('change', () => {
const selected = Array.from( const selected = Array.from(
langList.querySelectorAll('.lang-checkbox:checked') langList.querySelectorAll('.lang-checkbox:checked')
) ).map((cb) => tesseractLanguages[(cb as HTMLInputElement).value]);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
.map((cb) => tesseractLanguages[cb.value]);
selectedLangsDisplay.textContent = selectedLangsDisplay.textContent =
selected.length > 0 ? selected.join(', ') : 'None'; selected.length > 0 ? selected.join(', ') : 'None';
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message (processBtn as HTMLButtonElement).disabled = selected.length === 0;
processBtn.disabled = selected.length === 0;
}); });
// Attach the main OCR function to the process button
processBtn.addEventListener('click', runOCR); processBtn.addEventListener('click', runOCR);
} }
+25 -14
View File
@@ -3,29 +3,40 @@ import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib'; import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import JSZip from 'jszip';
export async function reversePages() { export async function reversePages() {
if (!state.pdfDoc) { const pdfDocs = state.files.filter(
(file: File) => file.type === 'application/pdf'
);
if (!pdfDocs.length) {
showAlert('Error', 'PDF not loaded.'); showAlert('Error', 'PDF not loaded.');
return; return;
} }
showLoader('Reversing page order...'); showLoader('Reversing page order...');
try { try {
const newPdf = await PDFLibDocument.create(); const zip = new JSZip();
const pageCount = state.pdfDoc.getPageCount(); for (let j = 0; j < pdfDocs.length; j++) {
const reversedIndices = Array.from( const file = pdfDocs[j];
{ length: pageCount }, const arrayBuffer = await file.arrayBuffer();
(_, i) => pageCount - 1 - i const pdfDoc = await PDFLibDocument.load(arrayBuffer);
); const newPdf = await PDFLibDocument.create();
const pageCount = pdfDoc.getPageCount();
const reversedIndices = Array.from(
{ length: pageCount },
(_, i) => pageCount - 1 - i
);
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices); const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page)); copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save(); const newPdfBytes = await newPdf.save();
downloadFile( const originalName = file.name.replace(/\.pdf$/i, '');
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), const fileName = `${originalName}_reversed.pdf`;
'reversed.pdf' zip.file(fileName, newPdfBytes);
); }
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'reversed_pdfs.zip');
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showAlert('Error', 'Could not reverse the PDF pages.'); showAlert('Error', 'Could not reverse the PDF pages.');
+98
View File
@@ -5,12 +5,87 @@ import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import '../css/styles.css'; import '../css/styles.css';
const hideBrandingSections = () => {
const nav = document.querySelector('nav');
if (nav) {
nav.style.display = 'none';
}
const heroSection = document.getElementById('hero-section');
if (heroSection) {
heroSection.style.display = 'none';
}
const featuresSection = document.getElementById('features-section');
if (featuresSection) {
featuresSection.style.display = 'none';
}
const securitySection = document.getElementById(
'security-compliance-section'
);
if (securitySection) {
securitySection.style.display = 'none';
}
const faqSection = document.getElementById('faq-accordion');
if (faqSection) {
faqSection.style.display = 'none';
}
const testimonialsSection = document.getElementById('testimonials-section');
if (testimonialsSection) {
testimonialsSection.style.display = 'none';
}
const supportSection = document.getElementById('support-section');
if (supportSection) {
supportSection.style.display = 'none';
}
const footer = document.querySelector('footer');
if (footer) {
footer.style.display = 'none';
}
const sectionDividers = document.querySelectorAll('.section-divider');
sectionDividers.forEach((divider) => {
(divider as HTMLElement).style.display = 'none';
});
document.title = 'PDF Tools';
const toolsHeader = document.getElementById('tools-header');
if (toolsHeader) {
const title = toolsHeader.querySelector('h2');
const subtitle = toolsHeader.querySelector('p');
if (title) {
title.textContent = 'PDF Tools';
title.className = 'text-4xl md:text-5xl font-bold text-white mb-3';
}
if (subtitle) {
subtitle.textContent = 'Select a tool to get started';
subtitle.className = 'text-lg text-gray-400';
}
}
const app = document.getElementById('app');
if (app) {
app.style.paddingTop = '2rem';
}
};
const init = () => { const init = () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url import.meta.url
).toString(); ).toString();
// Handle simple mode - hide branding sections
if (__SIMPLE_MODE__) {
hideBrandingSections();
}
dom.toolGrid.textContent = ''; dom.toolGrid.textContent = '';
categories.forEach((category) => { categories.forEach((category) => {
@@ -83,6 +158,29 @@ const init = () => {
}); });
}); });
window.addEventListener('keydown', function (e) {
const key = e.key.toLowerCase();
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
const isCtrlK = e.ctrlKey && key === 'k';
const isCmdK = isMac && e.metaKey && key === 'k';
if (isCtrlK || isCmdK) {
e.preventDefault();
searchBar.focus();
}
});
const shortcutK = document.getElementById('shortcut');
const isIosOrAndroid = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
if (isIosOrAndroid) {
shortcutK.style.display = 'none';
} else {
shortcutK.textContent = navigator.userAgent.toUpperCase().includes('MAC')
? '⌘ + K'
: 'Ctrl + K';
}
dom.toolGrid.addEventListener('click', (e) => { dom.toolGrid.addEventListener('click', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message // @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
const card = e.target.closest('.tool-card'); const card = e.target.closest('.tool-card');
+53
View File
@@ -0,0 +1,53 @@
document.addEventListener('DOMContentLoaded', () => {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
const menuIcon = document.getElementById('menu-icon');
const closeIcon = document.getElementById('close-icon');
if (mobileMenuButton && mobileMenu && menuIcon && closeIcon) {
// Toggle menu on button click
mobileMenuButton.addEventListener('click', () => {
const isExpanded =
mobileMenuButton.getAttribute('aria-expanded') === 'true';
// Toggle menu visibility
mobileMenu.classList.toggle('hidden');
// Toggle icons
menuIcon.classList.toggle('hidden');
closeIcon.classList.toggle('hidden');
// Update aria-expanded for accessibility
mobileMenuButton.setAttribute('aria-expanded', (!isExpanded).toString());
});
// Close menu when clicking on a link
const mobileLinks = mobileMenu.querySelectorAll('a');
mobileLinks.forEach((link) => {
link.addEventListener('click', () => {
mobileMenu.classList.add('hidden');
menuIcon.classList.remove('hidden');
closeIcon.classList.add('hidden');
mobileMenuButton.setAttribute('aria-expanded', 'false');
});
});
// Close menu when clicking outside
document.addEventListener('click', (event) => {
const target = event.target as Node;
const isClickInsideMenu = mobileMenu.contains(target);
const isClickOnButton = mobileMenuButton.contains(target);
if (
!isClickInsideMenu &&
!isClickOnButton &&
!mobileMenu.classList.contains('hidden')
) {
mobileMenu.classList.add('hidden');
menuIcon.classList.remove('hidden');
closeIcon.classList.add('hidden');
mobileMenuButton.setAttribute('aria-expanded', 'false');
}
});
}
});
+60 -4
View File
@@ -969,7 +969,7 @@ export const toolTemplates = {
'reverse-pages': () => ` 'reverse-pages': () => `
<h2 class="text-2xl font-bold text-white mb-4">Reverse PDF Pages</h2> <h2 class="text-2xl font-bold text-white mb-4">Reverse PDF Pages</h2>
<p class="mb-6 text-gray-400">Flip the order of all pages in your document, making the last page the first.</p> <p class="mb-6 text-gray-400">Flip the order of all pages in your document, making the last page the first.</p>
${createFileInputHTML()} ${createFileInputHTML({ multiple: true, accept: 'application/pdf', 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 btn-gradient w-full mt-6">Reverse & Download</button> <button id="process-btn" class="hidden btn-gradient w-full mt-6">Reverse & Download</button>
`, `,
@@ -1367,6 +1367,17 @@ export const toolTemplates = {
'ocr-pdf': () => ` 'ocr-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">OCR PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">OCR PDF</h2>
<p class="mb-6 text-gray-400">Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.</p> <p class="mb-6 text-gray-400">Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.</p>
<div class="p-3 bg-gray-900 rounded-lg border border-gray-700 mb-6">
<p class="text-sm text-gray-300"><strong class="text-white">How it works:</strong></p>
<ul class="list-disc list-inside text-xs text-gray-400 mt-1 space-y-1">
<li><strong class="text-white">Extract Text:</strong> Uses Tesseract OCR to recognize text from scanned images or PDFs.</li>
<li><strong class="text-white">Searchable Output:</strong> Creates a new PDF with an invisible text layer, making your document fully searchable while preserving the original appearance.</li>
<li><strong class="text-white">Character Filtering:</strong> Use whitelists to filter out unwanted characters and improve accuracy for specific document types (invoices, forms, etc.).</li>
<li><strong class="text-white">Multi-language Support:</strong> Select multiple languages for documents containing mixed language content.</li>
</ul>
</div>
${createFileInputHTML()} ${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
@@ -1393,7 +1404,10 @@ export const toolTemplates = {
<!-- Advanced settings section --> <!-- Advanced settings section -->
<details class="bg-gray-900 border border-gray-700 rounded-lg p-3"> <details class="bg-gray-900 border border-gray-700 rounded-lg p-3">
<summary class="text-sm font-medium text-gray-300 cursor-pointer">Advanced Settings</summary> <summary class="text-sm font-medium text-gray-300 cursor-pointer flex items-center justify-between">
<span>Advanced Settings (Recommended to improve accuracy)</span>
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform details-icon"></i>
</summary>
<div class="mt-4 space-y-4"> <div class="mt-4 space-y-4">
<!-- Resolution Setting --> <!-- Resolution Setting -->
<div> <div>
@@ -1409,10 +1423,28 @@ export const toolTemplates = {
<input type="checkbox" id="ocr-binarize" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600"> <input type="checkbox" id="ocr-binarize" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600">
Binarize Image (Enhance Contrast for Clean Scans) Binarize Image (Enhance Contrast for Clean Scans)
</label> </label>
<!-- Character Whitelist -->
<!-- Character Whitelist Presets -->
<div>
<label for="whitelist-preset" class="block mb-1 text-xs font-medium text-gray-400">Character Whitelist Preset</label>
<select id="whitelist-preset" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm mb-2">
<option value="">None (All characters)</option>
<option value="alphanumeric">Alphanumeric + Basic Punctuation</option>
<option value="numbers-currency">Numbers + Currency Symbols</option>
<option value="letters-only">Letters Only (A-Z, a-z)</option>
<option value="numbers-only">Numbers Only (0-9)</option>
<option value="invoice">Invoice/Receipt (Numbers, $, ., -, /)</option>
<option value="forms">Forms (Alphanumeric + Common Symbols)</option>
<option value="custom">Custom...</option>
</select>
<p class="text-xs text-gray-500 mt-1">Only these characters will be recognized. Leave empty for all characters.</p>
</div>
<!-- Character Whitelist Input -->
<div> <div>
<label for="ocr-whitelist" class="block mb-1 text-xs font-medium text-gray-400">Character Whitelist (Optional)</label> <label for="ocr-whitelist" class="block mb-1 text-xs font-medium text-gray-400">Character Whitelist (Optional)</label>
<input type="text" id="ocr-whitelist" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm" placeholder="e.g., abcdefghijklmnopqrstuvwxyz0123456789$.,"> <input type="text" id="ocr-whitelist" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm" placeholder="e.g., abcdefghijklmnopqrstuvwxyz0123456789$.,">
<p class="text-xs text-gray-500 mt-1">Only these characters will be recognized. Leave empty for all characters.</p>
</div> </div>
</div> </div>
</details> </details>
@@ -1847,11 +1879,35 @@ export const toolTemplates = {
</div> </div>
`, `,
linearize: () => ` linearize: () => `
<h2 class="text-2xl font-bold text-white mb-4">Linearize PDFs (Fast Web View)</h2> <h2 class="text-2xl font-bold text-white mb-4">Linearize PDFs (Fast Web View)</h2>
<p class="mb-6 text-gray-400">Optimize multiple PDFs for faster loading over the web. Files will be downloaded in a ZIP archive.</p> <p class="mb-6 text-gray-400">Optimize multiple PDFs for faster loading over the web. Files will be downloaded in a ZIP archive.</p>
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })} ${createFileInputHTML({ multiple: true, accept: 'application/pdf', 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 btn-gradient w-full mt-6" disabled>Linearize PDFs & Download ZIP</button> <button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Linearize PDFs & Download ZIP</button>
`, `,
'add-attachments': () => `
<h2 class="text-2xl font-bold text-white mb-4">Add Attachments to PDF</h2>
<p class="mb-6 text-gray-400">First, upload the PDF document you want to add files to.</p>
${createFileInputHTML({ accept: 'application/pdf' })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="attachment-options" class="hidden mt-8">
<h3 class="text-lg font-semibold text-white mb-3">Upload Files to Attach</h3>
<p class="mb-4 text-gray-400">Select one or more files to embed within the PDF. You can attach any file type (images, documents, spreadsheets, etc.).</p>
<label for="attachment-files-input" class="w-full flex justify-center items-center px-6 py-10 bg-gray-900 text-gray-400 rounded-lg border-2 border-dashed border-gray-600 hover:bg-gray-800 hover:border-gray-500 cursor-pointer transition-colors">
<div class="text-center">
<svg class="mx-auto h-12 w-12" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true"><path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
<span class="mt-2 block text-sm font-medium">Click to upload files</span>
<span class="mt-1 block text-xs">Any file type, multiple files allowed</span>
</div>
<input id="attachment-files-input" name="attachment-files" type="file" class="sr-only" multiple>
</label>
<div id="attachment-file-list" class="mt-4 space-y-2"></div>
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button>
</div>
`,
}; };
+31 -8
View File
@@ -181,7 +181,10 @@ describe('Add Blank Page Tool', () => {
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [0, 1]); expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [0, 1]);
// Should add 1 blank page + 5 existing pages = 6 total calls // Should add 1 blank page + 5 existing pages = 6 total calls
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(6); expect(mockNewDoc.addPage).toHaveBeenCalledTimes(6);
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [2, 3, 4]); expect(mockNewDoc.copyPages).toHaveBeenCalledWith(
state.pdfDoc,
[2, 3, 4]
);
expect(helpers.downloadFile).toHaveBeenCalledWith( expect(helpers.downloadFile).toHaveBeenCalledWith(
expect.any(Blob), expect.any(Blob),
'blank-page-added.pdf' 'blank-page-added.pdf'
@@ -196,7 +199,10 @@ describe('Add Blank Page Tool', () => {
await addBlankPage(); await addBlankPage();
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [0, 1, 2, 3, 4]); expect(mockNewDoc.copyPages).toHaveBeenCalledWith(
state.pdfDoc,
[0, 1, 2, 3, 4]
);
// Should add 1 blank page + 5 existing pages = 6 total calls // Should add 1 blank page + 5 existing pages = 6 total calls
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(6); expect(mockNewDoc.addPage).toHaveBeenCalledTimes(6);
// When adding at the end, there are no pages after, so copyPages is not called for indicesAfter // When adding at the end, there are no pages after, so copyPages is not called for indicesAfter
@@ -239,7 +245,10 @@ describe('Add Blank Page Tool', () => {
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [0, 1]); expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [0, 1]);
// Should add 5 blank pages + 5 existing pages = 10 total calls // Should add 5 blank pages + 5 existing pages = 10 total calls
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(10); expect(mockNewDoc.addPage).toHaveBeenCalledTimes(10);
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [2, 3, 4]); expect(mockNewDoc.copyPages).toHaveBeenCalledWith(
state.pdfDoc,
[2, 3, 4]
);
expect(helpers.downloadFile).toHaveBeenCalledWith( expect(helpers.downloadFile).toHaveBeenCalledWith(
expect.any(Blob), expect.any(Blob),
'blank-pages-added.pdf' 'blank-pages-added.pdf'
@@ -255,7 +264,10 @@ describe('Add Blank Page Tool', () => {
await addBlankPage(); await addBlankPage();
expect(ui.showLoader).toHaveBeenCalledWith('Adding 2 blank pages...'); expect(ui.showLoader).toHaveBeenCalledWith('Adding 2 blank pages...');
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [0, 1, 2, 3, 4]); expect(mockNewDoc.copyPages).toHaveBeenCalledWith(
state.pdfDoc,
[0, 1, 2, 3, 4]
);
// Should add 2 blank pages + 5 existing pages = 7 total calls // Should add 2 blank pages + 5 existing pages = 7 total calls
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(7); expect(mockNewDoc.addPage).toHaveBeenCalledTimes(7);
expect(helpers.downloadFile).toHaveBeenCalledWith( expect(helpers.downloadFile).toHaveBeenCalledWith(
@@ -268,7 +280,9 @@ describe('Add Blank Page Tool', () => {
// -------------------- Error Handling Tests -------------------- // -------------------- Error Handling Tests --------------------
describe('Error Handling', () => { describe('Error Handling', () => {
it('should handle PDF creation errors', async () => { it('should handle PDF creation errors', async () => {
vi.mocked(PDFLibDocument.create).mockRejectedValue(new Error('PDF creation failed')); vi.mocked(PDFLibDocument.create).mockRejectedValue(
new Error('PDF creation failed')
);
document.body.innerHTML = ` document.body.innerHTML = `
<input id="page-number" value="2" /> <input id="page-number" value="2" />
@@ -277,7 +291,10 @@ describe('Add Blank Page Tool', () => {
await addBlankPage(); await addBlankPage();
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not add blank page.'); expect(ui.showAlert).toHaveBeenCalledWith(
'Error',
'Could not add blank page.'
);
expect(ui.hideLoader).toHaveBeenCalled(); expect(ui.hideLoader).toHaveBeenCalled();
}); });
@@ -291,7 +308,10 @@ describe('Add Blank Page Tool', () => {
await addBlankPage(); await addBlankPage();
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not add blank page.'); expect(ui.showAlert).toHaveBeenCalledWith(
'Error',
'Could not add blank page.'
);
expect(ui.hideLoader).toHaveBeenCalled(); expect(ui.hideLoader).toHaveBeenCalled();
}); });
@@ -305,7 +325,10 @@ describe('Add Blank Page Tool', () => {
await addBlankPage(); await addBlankPage();
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not add blank page.'); expect(ui.showAlert).toHaveBeenCalledWith(
'Error',
'Could not add blank page.'
);
expect(ui.hideLoader).toHaveBeenCalled(); expect(ui.hideLoader).toHaveBeenCalled();
}); });
}); });
+1 -1
View File
@@ -61,7 +61,7 @@ describe('Tool Configuration Arrays', () => {
}); });
it('should have the correct number of tools', () => { it('should have the correct number of tools', () => {
expect(multiFileTools).toHaveLength(12); expect(multiFileTools).toHaveLength(13);
}); });
it('should not contain any duplicate tools', () => { it('should not contain any duplicate tools', () => {
+140
View File
@@ -0,0 +1,140 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { reversePages } from '../js/logic/reverse-pages';
import { state } from '../js/state';
import * as helpers from '../js/utils/helpers';
import * as ui from '../js/ui';
import JSZip from 'jszip';
vi.mock('../js/ui', () => ({
showLoader: vi.fn(),
hideLoader: vi.fn(),
showAlert: vi.fn(),
}));
vi.mock('../js/utils/helpers', () => ({
downloadFile: vi.fn(),
}));
vi.mock('pdf-lib', () => ({
PDFDocument: {
create: vi.fn(),
load: vi.fn().mockResolvedValue({
getPageCount: vi.fn(() => 2),
copyPages: vi.fn((_, indices) =>
Promise.resolve(indices.map((i) => ({ page: `page-${i}` })))
),
}),
},
}));
describe('reversePages - multi PDF support', () => {
let mockNewDoc: any;
beforeEach(() => {
state.files = []; // ✅ now using files, not pdfDocs
mockNewDoc = {
copyPages: vi.fn((doc: any, indices: number[]) =>
Promise.resolve(indices.map((i: number) => ({ page: `page-${i}` })))
),
addPage: vi.fn(),
save: vi.fn(() => Promise.resolve(new Uint8Array([1, 2, 3]))),
};
vi.mocked(PDFLibDocument.create).mockResolvedValue(mockNewDoc);
vi.mocked(helpers.downloadFile).mockImplementation(() => {});
vi.mocked(ui.showLoader).mockImplementation(() => {});
vi.mocked(ui.hideLoader).mockImplementation(() => {});
vi.mocked(ui.showAlert).mockImplementation(() => {});
});
afterEach(() => {
vi.clearAllMocks();
});
it('should reverse pages for multiple PDFs and create a zip', async () => {
const mockFile = (name: string) => ({
name,
type: 'application/pdf',
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
});
state.files = [mockFile('a.pdf'), mockFile('b.pdf')]; // ✅ now matches function
await reversePages();
expect(helpers.downloadFile).toHaveBeenCalledWith(
expect.any(Blob),
'reversed_pdfs.zip'
);
expect(mockNewDoc.copyPages).toHaveBeenCalledTimes(2);
expect(mockNewDoc.addPage).toHaveBeenCalled();
expect(mockNewDoc.save).toHaveBeenCalledTimes(2);
});
it('should handle empty PDF list gracefully', async () => {
state.files = [];
await reversePages();
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'PDF not loaded.');
});
it('should handle PDF creation errors', async () => {
vi.mocked(PDFLibDocument.create).mockRejectedValue(
new Error('Create failed')
);
state.files = [
{
name: 'x.pdf',
type: 'application/pdf',
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
},
];
await reversePages();
expect(ui.showAlert).toHaveBeenCalledWith(
'Error',
'Could not reverse the PDF pages.'
);
expect(ui.hideLoader).toHaveBeenCalled();
});
it('should handle PDF processing errors', async () => {
mockNewDoc.copyPages.mockRejectedValue(new Error('Copy failed'));
state.files = [
{
name: 'y.pdf',
type: 'application/pdf',
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
},
];
await reversePages();
expect(ui.showAlert).toHaveBeenCalledWith(
'Error',
'Could not reverse the PDF pages.'
);
expect(ui.hideLoader).toHaveBeenCalled();
});
it('should handle save errors', async () => {
mockNewDoc.save.mockRejectedValue(new Error('Save failed'));
state.files = [
{
name: 'z.pdf',
type: 'application/pdf',
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
},
];
await reversePages();
expect(ui.showAlert).toHaveBeenCalledWith(
'Error',
'Could not reverse the PDF pages.'
);
expect(ui.hideLoader).toHaveBeenCalled();
});
});
+1
View File
@@ -0,0 +1 @@
declare const __SIMPLE_MODE__: boolean;
+71 -9
View File
@@ -14,17 +14,21 @@
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
<a <div
href="index.html"
class="flex-shrink-0 flex items-center cursor-pointer" class="flex-shrink-0 flex items-center cursor-pointer"
id="home-logo"
> >
<img <img
src="images/favicon.svg" src="images/favicon.svg"
alt="BentoPDF Logo" alt="Bento PDF Logo"
class="h-8 w-8 mr-2" class="h-8 w-8"
/> />
<span class="text-white font-bold text-xl">BentoPDF</span> <span class="text-white font-bold text-xl ml-2">
</a> <a href="index.html">BentoPDF</a>
</span>
</div>
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8"> <div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a> <a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a> <a href="./about.html" class="nav-link">About</a>
@@ -32,12 +36,69 @@
<a href="index.html#tools-header" class="nav-link">All Tools</a> <a href="index.html#tools-header" class="nav-link">All Tools</a>
</div> </div>
<div class="md:hidden flex items-center gap-4"> <!-- Mobile Hamburger Button -->
<a href="index.html" class="nav-link">Home</a> <div class="md:hidden flex items-center">
<a href="index.html#tools-header" class="nav-link">All Tools</a> <button
id="mobile-menu-button"
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
aria-controls="mobile-menu"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<!-- Hamburger Icon -->
<svg
id="menu-icon"
class="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
<!-- Close Icon -->
<svg
id="close-icon"
class="hidden h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div> </div>
</div> </div>
</div> </div>
<!-- Mobile Menu Dropdown -->
<div
id="mobile-menu"
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
>
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
<a href="index.html" class="mobile-nav-link">Home</a>
<a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a>
<a href="index.html#tools-header" class="mobile-nav-link"
>All Tools</a
>
</div>
</div>
</nav> </nav>
<div id="app" class="container mx-auto p-4 md:p-8"> <div id="app" class="container mx-auto p-4 md:p-8">
<section class="max-w-4xl mx-auto py-12"> <section class="max-w-4xl mx-auto py-12">
@@ -249,5 +310,6 @@
<script> <script>
lucide.createIcons(); lucide.createIcons();
</script> </script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -24,7 +24,7 @@ https://hub.docker.com/r/bentopdf/bentopdf/</Overview>
<DonateText/> <DonateText/>
<DonateLink/> <DonateLink/>
<Requires/> <Requires/>
<Config Name="Container Port 1" Target="3000" Default="80" Mode="tcp" Description="" Type="Port" Display="always" Required="false" Mask="false">80</Config> <Config Name="Web Port for GUI" Target="3000" Default="80" Mode="tcp" Description="Port used for BentoPDF's web GUI" Type="Port" Display="always" Required="false" Mask="false">80</Config>
<TailscaleStateDir/> <TailscaleStateDir/>
</Container> </Container>
+5 -2
View File
@@ -3,7 +3,7 @@ import tailwindcss from '@tailwindcss/vite';
import { nodePolyfills } from 'vite-plugin-node-polyfills'; import { nodePolyfills } from 'vite-plugin-node-polyfills';
import { resolve } from 'path'; import { resolve } from 'path';
export default defineConfig({ export default defineConfig(({ mode }) => ({
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
nodePolyfills({ nodePolyfills({
@@ -15,6 +15,9 @@ export default defineConfig({
}, },
}), }),
], ],
define: {
__SIMPLE_MODE__: JSON.stringify(process.env.SIMPLE_MODE === 'true'),
},
resolve: { resolve: {
alias: { alias: {
stream: 'stream-browserify', stream: 'stream-browserify',
@@ -52,4 +55,4 @@ export default defineConfig({
], ],
}, },
}, },
}); }));