setup i18n and ported all tools to standalone pages

This commit is contained in:
abdullahalam123
2025-12-11 19:34:14 +05:30
parent fe3e54f979
commit 78dc6333f9
221 changed files with 30351 additions and 11131 deletions

View File

@@ -6,9 +6,11 @@ RUN npm ci
COPY . .
# Build without type checking (vite build only)
# Pass SIMPLE_MODE environment variable if provided
# Pass SIMPLE_MODE and BASE_URL environment variables if provided
ARG SIMPLE_MODE=false
ARG BASE_URL=/
ENV SIMPLE_MODE=$SIMPLE_MODE
ENV BASE_URL=$BASE_URL
RUN npm run build -- --mode production
# Production stage
@@ -16,10 +18,44 @@ FROM nginxinc/nginx-unprivileged:stable-alpine-slim
LABEL org.opencontainers.image.source="https://github.com/alam00000/bentopdf"
COPY --chown=nginx:nginx --from=builder /app/dist /usr/share/nginx/html
ARG BASE_URL=/
ENV BASE_URL=$BASE_URL
# Switch to root to create directories and copy files (dont worry guys, its not a security issue as we switch it only for the duration of the build and its needed to create the destination directory based on BASE_URL)
USER root
COPY --chown=nginx:nginx nginx.conf /etc/nginx/nginx.conf
RUN set -e; \
SUBDIR=$(echo "${BASE_URL}" | sed 's:^/::; s:/$::'); \
if [ -z "${SUBDIR}" ] || [ "${SUBDIR}" = "/" ]; then \
DEST_DIR="/usr/share/nginx/html"; \
else \
DEST_DIR="/usr/share/nginx/html/${SUBDIR}"; \
mkdir -p "${DEST_DIR}"; \
fi; \
chown -R nginx:nginx /usr/share/nginx/html; \
echo "Destination directory: ${DEST_DIR}"
COPY --chown=nginx:nginx --from=builder /app/dist /tmp/dist
RUN set -e; \
SUBDIR=$(echo "${BASE_URL}" | sed 's:^/::; s:/$::'); \
if [ -z "${SUBDIR}" ] || [ "${SUBDIR}" = "/" ]; then \
DEST_DIR="/usr/share/nginx/html"; \
else \
DEST_DIR="/usr/share/nginx/html/${SUBDIR}"; \
fi; \
cp -r /tmp/dist/* "${DEST_DIR}/"; \
rm -rf /tmp/dist; \
chown -R nginx:nginx /usr/share/nginx/html; \
echo "Files copied to: ${DEST_DIR}"; \
ls -la "${DEST_DIR}" | head -20
RUN mkdir -p /etc/nginx/tmp && chown -R nginx:nginx /etc/nginx/tmp
USER nginx
EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -10,6 +10,14 @@
---
## 📢 Join Us on Discord
[![Discord](https://img.shields.io/badge/Discord-Join%20Server-7289da?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/Bgq3Ay3f2w)
Have questions, feature requests, or want to chat with the community? Join our Discord server!
---
## 📜 Licensing
BentoPDF is dual-licensed:
@@ -139,6 +147,19 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
---
## 🌍 Translations
BentoPDF is available in multiple languages:
| Language | Status |
|----------|--------|
| English | [![English](https://img.shields.io/badge/Complete-green?style=flat-square)](public/locales/en/common.json) |
| German | [![German](https://img.shields.io/badge/In_Progress-yellow?style=flat-square)](public/locales/de/common.json) |
Want to help translate BentoPDF into your language? Check out our [Translation Guide](TRANSLATION.md)!
---
## 🚀 Getting Started
You can run BentoPDF locally for development or personal use.
@@ -279,6 +300,34 @@ The website can be accessible at: ```http://localhost:3000/tools/bentopdf/```
The `npm run package` command creates a `dist-{version}.zip` file that you can use for self-hosting.
**Docker Subdirectory Deployment:**
BentoPDF's Docker image also supports the `BASE_URL` build argument for subdirectory deployments:
```bash
# Build for subdirectory deployment
docker build --build-arg BASE_URL=/bentopdf/ -t bentopdf .
# Run the container
docker run -p 3000:8080 bentopdf
# The app will be accessible at http://localhost:3000/bentopdf/
```
**Combined with Simple Mode:**
```bash
# Build with both BASE_URL and SIMPLE_MODE
docker build \
--build-arg BASE_URL=/tools/pdf/ \
--build-arg SIMPLE_MODE=true \
-t bentopdf-simple .
docker run -p 3000:8080 bentopdf-simple
```
> **Important**: Always include trailing slashes in `BASE_URL` (e.g., `/bentopdf/` not `/bentopdf`). The default value is `/` for root deployment.
### 🚀 Run with Docker Compose (Recommended)
For a more robust setup with auto-restart capabilities:
@@ -437,7 +486,7 @@ BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript*
- **PDF to Office**: Converts PDF files into editable Word, Excel, and PowerPoint formats.
- **Office to PDF**: Converts Word, Excel, and PowerPoint documents into optimized PDFs.
Contributions and discussions on the roadmap are welcome! Join the conversation via [Discord](https://discord.gg/AP2Y97juZT).
Contributions and discussions on the roadmap are welcome! Join the conversation via [Discord](https://discord.gg/Bgq3Ay3f2w).
---

478
TRANSLATION.md Normal file
View File

@@ -0,0 +1,478 @@
# 🌍 Translation Guide for BentoPDF
This guide will help you add new languages or improve existing translations for BentoPDF.
## Table of Contents
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Adding a New Language](#adding-a-new-language)
- [Translation File Structure](#translation-file-structure)
- [Where Translations Are Used](#where-translations-are-used)
- [Testing Your Translations](#testing-your-translations)
- [Translation Guidelines](#translation-guidelines)
- [Common Issues](#common-issues)
---
## Overview
BentoPDF uses **i18next** for internationalization (i18n). Currently supported languages:
- **English** (`en`) - Default
- **German** (`de`)
The app automatically detects the language from the URL path:
- `/en/` → English
- `/de/` → German
---
## Quick Start
**To improve existing translations:**
1. Navigate to `public/locales/{language}/common.json`
2. Find the key you want to update
3. Change the translation value
4. Save and test
**To add a new language (e.g., Spanish):**
1. Copy `public/locales/en/common.json` to `public/locales/es/common.json`
2. Translate all values in `es/common.json`
3. Add Spanish to `supportedLanguages` in `src/js/i18n/i18n.ts`
4. Add Spanish name to `languageNames` in `src/js/i18n/i18n.ts`
5. Test thoroughly
---
## Adding a New Language
Let's add **French** as an example:
### Step 1: Create Translation File
```bash
# Create the directory
mkdir -p public/locales/fr
# Copy the English template
cp public/locales/en/common.json public/locales/fr/common.json
```
### Step 2: Translate the JSON File
Open `public/locales/fr/common.json` and translate all the values:
```json
{
"nav": {
"home": "Accueil",
"about": "À propos",
"contact": "Contact",
"allTools": "Tous les outils"
},
"hero": {
"title": "Votre boîte à outils PDF gratuite et sécurisée",
"subtitle": "Fusionnez, divisez, compressez et modifiez des PDF directement dans votre navigateur."
}
// ... continue translating all keys
}
```
⚠️ **Important**: Only translate the **values**, NOT the keys!
**Correct:**
```json
"home": "Accueil"
```
**Wrong:**
```json
"accueil": "Accueil"
```
### Step 3: Register the Language
Edit `src/js/i18n/i18n.ts`:
```typescript
// Add 'fr' to supported languages
export const supportedLanguages = ['en', 'de', 'fr'] as const;
export type SupportedLanguage = (typeof supportedLanguages)[number];
// Add French display name
export const languageNames: Record<SupportedLanguage, string> = {
en: 'English',
de: 'Deutsch',
fr: 'Français', // ← Add this
};
```
### Step 4: Test Your Translation
```bash
# Start the dev server
npm run dev
# Visit the French version
# http://localhost:5173/fr/
```
---
## Translation File Structure
The `common.json` file is organized into logical sections:
```json
{
"nav": {
// Navigation menu items
},
"hero": {
// Homepage hero section
},
"features": {
// Features section
},
"tools": {
// Tool names and descriptions
},
"upload": {
// File upload UI
},
"settings": {
// Settings modal and keyboard shortcuts
},
"faq": {
// FAQ section
},
"footer": {
// Footer links and text
},
"compliance": {
// Security compliance information
},
"testimonials": {
// User testimonials
},
"support": {
// Support section
},
"alert": {
// Alert and error messages
}
}
```
### Key Naming Convention
- Use **camelCase** for keys: `"deletePage"` not `"delete_page"`
- Use **nested objects** for organization: `"nav.home"` is represented as:
```json
{
"nav": {
"home": "Home"
}
}
```
- Be descriptive: `"shortcutsWarning"` is better than `"warning1"`
---
## Where Translations Are Used
### 1. HTML Templates (`data-i18n` attribute)
```html
<!-- Translation key: nav.home -->
<a href="/" data-i18n="nav.home">Home</a>
```
The `data-i18n` attribute tells i18next which translation to use.
### 2. Tool Definitions
Tool names and descriptions are defined in `src/js/config/tools.ts` and use a special namespace:
```typescript
{
name: 'Merge PDF', // Used for shortcuts only
subtitle: 'Combine multiple PDFs into one file.',
}
```
In translations:
```json
{
"tools": {
"mergePdf": {
"name": "PDF zusammenführen",
"subtitle": "Mehrere PDFs in eine Datei kombinieren."
}
}
}
```
### 3. Dynamic JavaScript (`t()` function)
For translations that need to be applied dynamically:
```typescript
import { t } from './i18n/i18n';
const message = t('alert.error');
console.log(message); // "Error" or "Fehler" depending on language
```
### 4. Placeholders
For input placeholders:
```html
<input
type="text"
placeholder="Search for a tool..."
data-i18n-placeholder="tools.searchPlaceholder"
/>
```
In `common.json`:
```json
{
"tools": {
"searchPlaceholder": "Nach einem Tool suchen..."
}
}
```
---
## Testing Your Translations
### Manual Testing
1. **Start development server:**
```bash
npm run dev
```
2. **Visit each language:**
- English: `http://localhost:5173/en/`
- German: `http://localhost:5173/de/`
- Your new language: `http://localhost:5173/fr/`
3. **Check these pages:**
- Homepage (`/`)
- About page (`/about.html`)
- Contact page (`/contact.html`)
- FAQ page (`/faq.html`)
- Tool pages (e.g., `/merge-pdf.html`)
4. **Test these interactions:**
- Click the language switcher in the footer
- Navigate between pages
- Open the settings modal (click gear icon next to search)
- Try a tool to see upload messages
### Automated Checks
Check for missing translations:
```bash
# This will show any missing keys
node scripts/check-translations.js
```
*(If this script doesn't exist, you may need to create it or manually compare JSON files)*
### Browser Testing
Test in different browsers:
- Chrome/Edge
- Firefox
- Safari
---
## Translation Guidelines
### 1. Keep the Tone Consistent
BentoPDF is **friendly, clear, and professional**. Match this tone in your translations.
✅ **Good:**
```json
"hero.title": "Ihr kostenloses und sicheres PDF-Toolkit"
```
❌ **Too formal:**
```json
"hero.title": "Ihr gebührenfreies und gesichertes Werkzeug für PDF-Dokumente"
```
### 2. Preserve Formatting
Some strings contain HTML or special characters:
```json
{
"faq.analytics.answer": "We care about your privacy. BentoPDF does not track personal information. We use <a href=\"https://simpleanalytics.com\" class=\"text-indigo-400 hover:underline\" target=\"_blank\" rel=\"noopener noreferrer\">Simple Analytics</a> solely to see anonymous visit counts."
}
```
When translating, **keep the HTML tags intact**:
```json
{
"faq.analytics.answer": "Wir schätzen Ihre Privatsphäre. BentoPDF verfolgt keine persönlichen Informationen. Wir verwenden <a href=\"https://simpleanalytics.com\" class=\"text-indigo-400 hover:underline\" target=\"_blank\" rel=\"noopener noreferrer\">Simple Analytics</a> ausschließlich, um anonyme Besucherzahlen zu sehen."
}
```
### 3. Handle Plurals and Gender
If your language has complex plural rules or gender distinctions, consult the [i18next pluralization guide](https://www.i18next.com/translation-function/plurals).
Example:
```json
{
"pages": "page",
"pages_plural": "pages"
}
```
### 4. Don't Translate Brand Names or Legal Terms
Keep these as-is:
- BentoPDF
- PDF
- GitHub
- Discord
- Chrome, Firefox, Safari, etc.
- Terms and Conditions
- Privacy Policy
- Licensing
### 5. Technical Terms
For technical terms, use commonly accepted translations in your language:
- "Merge" → "Fusionner" (French), "Zusammenführen" (German)
- "Split" → "Diviser" (French), "Teilen" (German)
- "Compress" → "Compresser" (French), "Komprimieren" (German)
If unsure, check how other PDF tools translate these terms in your language.
### 6. String Length
Some UI elements have limited space. Try to keep translations **similar in length** to the English version.
If a translation is much longer, test it visually to ensure it doesn't break the layout.
---
## Common Issues
### Issue: Translations Not Showing Up
**Solution:**
1. Clear your browser cache
2. Hard refresh (Ctrl+F5 or Cmd+Shift+R)
3. Check browser console for errors
4. Verify the JSON file is valid (no syntax errors)
### Issue: Some Text Still in English
**Possible causes:**
1. Missing translation key in your language file
2. Missing `data-i18n` attribute in HTML
3. Hardcoded text in JavaScript
**Solution:**
- Compare your language file with `en/common.json` to find missing keys
- Search the codebase for hardcoded strings
### Issue: JSON Syntax Error
**Symptoms:**
```
SyntaxError: Unexpected token } in JSON at position 1234
```
**Solution:**
- Use a JSON validator: https://jsonlint.com/
- Common mistakes:
- Trailing comma after last item
- Missing or extra quotes
- Unescaped quotes inside strings (use `\"`)
### Issue: Language Switcher Not Showing New Language
**Solution:**
Make sure you added the language to both arrays in `i18n.ts`:
```typescript
export const supportedLanguages = ['en', 'de', 'fr']; // ← Add here
export const languageNames = {
en: 'English',
de: 'Deutsch',
fr: 'Français', // ← And here
};
```
---
## File Checklist
When adding a new language, make sure these files are updated:
- [ ] `public/locales/{lang}/common.json` - Main translation file
- [ ] `src/js/i18n/i18n.ts` - Add to `supportedLanguages` and `languageNames`
- [ ] Test all pages: homepage, about, contact, FAQ, tool pages
- [ ] Test settings modal and shortcuts
- [ ] Test language switcher in footer
- [ ] Verify URL routing works (`/{lang}/`)
---
## Getting Help
If you have questions or need help:
1. Check existing translations in `public/locales/de/common.json` for reference
2. Open an issue on [GitHub](https://github.com/alam00000/bentopdf/issues)
3. Join our [Discord server](https://discord.gg/Bgq3Ay3f2w)
---
## Contributing Your Translation
Once you've completed a translation:
1. **Test thoroughly** (see [Testing Your Translations](#testing-your-translations))
2. **Fork the repository** on GitHub
3. **Create a new branch**: `git checkout -b add-french-translation`
4. **Commit your changes**: `git commit -m "Add French translation"`
5. **Push to your fork**: `git push origin add-french-translation`
6. **Open a Pull Request** with:
- Description of the language added
- Screenshots showing the translation in action
- Confirmation that you've tested all pages
Thank you for contributing to BentoPDF! 🎉
---
## Translation Progress
Current translation coverage:
| Language | Code | Status | Maintainer |
|----------|------|--------|------------|
| English | `en` | ✅ Complete | Core team |
| German | `de` | 🚧 In Progress | Core team |
| Your Language | `??` | 🚧 In Progress | You? |
---
**Last Updated**: December 2025

View File

@@ -5,6 +5,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>About Bentopdf - Fast, Private, and Free PDF Tools</title>
<link rel="alternate" hreflang="en" href="/en/about.html" />
<link rel="alternate" hreflang="de" href="/de/about.html" />
<link rel="alternate" hreflang="x-default" href="/en/about.html" />
<link href="/src/css/styles.css" rel="stylesheet" />
</head>
@@ -24,11 +27,11 @@
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
<a href="index.html" class="nav-link" data-i18n="nav.home">Home</a>
<a href="./about.html" class="nav-link" data-i18n="nav.about">About</a>
<a href="./contact.html" class="nav-link" data-i18n="nav.contact">Contact</a>
<a href="./licensing.html" class="nav-link" data-i18n="nav.licensing">Licensing</a>
<a href="index.html#tools-header" class="nav-link" data-i18n="nav.allTools">All Tools</a>
</div>
<!-- Mobile Hamburger Button -->
@@ -55,11 +58,11 @@
<!-- 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="./licensing.html" class="mobile-nav-link">Licensing</a>
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
<a href="index.html" class="mobile-nav-link" data-i18n="nav.home">Home</a>
<a href="./about.html" class="mobile-nav-link" data-i18n="nav.about">About</a>
<a href="./contact.html" class="mobile-nav-link" data-i18n="nav.contact">Contact</a>
<a href="./licensing.html" class="mobile-nav-link" data-i18n="nav.licensing">Licensing</a>
<a href="index.html#tools-header" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
</div>
</div>
</nav>
@@ -67,10 +70,10 @@
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
<section id="about-hero" class="text-center py-16 md:py-24">
<h1 class="text-3xl md:text-6xl font-bold text-white mb-4">
We believe PDF tools should be
<span class="marker-slanted">fast, private, and free.</span>
<span data-i18n="about.hero.title">We believe PDF tools should be</span>
<span class="marker-slanted" data-i18n="about.hero.subtitle">fast, private, and free.</span>
</h1>
<p class="text-lg md:text-xl text-gray-400">No compromises.</p>
<p class="text-lg md:text-xl text-gray-400" data-i18n="about.hero.noCompromises">No compromises.</p>
</section>
<div class="section-divider"></div>
@@ -78,10 +81,10 @@
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
<div class="text-center">
<i data-lucide="rocket" class="w-16 h-16 text-indigo-400 mx-auto mb-6"></i>
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4" data-i18n="about.mission.title">
Our Mission
</h2>
<p class="text-lg text-gray-400 leading-relaxed">
<p class="text-lg text-gray-400 leading-relaxed" data-i18n="about.mission.description">
To provide the most comprehensive PDF toolbox that respects your
privacy and never asks for payment. We believe essential document
tools should be accessible to everyone, everywhere, without
@@ -93,11 +96,12 @@
<div class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700">
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
<div class="text-center md:text-left">
<span class="text-indigo-400 font-bold uppercase">Our Core Philosophy</span>
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">
<span class="text-indigo-400 font-bold uppercase" data-i18n="about.philosophy.label">Our Core
Philosophy</span>
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4" data-i18n="about.philosophy.title">
Privacy First. Always.
</h2>
<p class="text-gray-400 leading-relaxed">
<p class="text-gray-400 leading-relaxed" data-i18n="about.philosophy.description">
In an era where data is a commodity, we take a different approach.
All processing for Bentopdf tools happens locally in your browser.
This means your files never touch our servers, we never see your
@@ -118,14 +122,14 @@
<section id="why-Bentopdf" class="py-16">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
Why <span class="marker-slanted">Bentopdf?</span>
<span data-i18n="about.whyBentopdf.title">Why</span> <span class="marker-slanted">Bentopdf?</span>
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-xl font-bold text-white">Built for Speed</h3>
<p class="text-gray-400 mt-2">
<h3 class="text-xl font-bold text-white" data-i18n="about.whyBentopdf.speed.title">Built for Speed</h3>
<p class="text-gray-400 mt-2" data-i18n="about.whyBentopdf.speed.description">
No waiting for uploads or downloads to a server. By processing
files directly in your browser using modern web technologies
like WebAssembly, we offer unparalleled speed for all our tools.
@@ -135,8 +139,8 @@
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
<i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-xl font-bold text-white">Completely Free</h3>
<p class="text-gray-400 mt-2">
<h3 class="text-xl font-bold text-white" data-i18n="about.whyBentopdf.free.title">Completely Free</h3>
<p class="text-gray-400 mt-2" data-i18n="about.whyBentopdf.free.description">
No trials, no subscriptions, no hidden fees, and no "premium"
features held hostage. We believe powerful PDF tools should be a
public utility, not a profit center.
@@ -146,8 +150,9 @@
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
<i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-xl font-bold text-white">No Account Required</h3>
<p class="text-gray-400 mt-2">
<h3 class="text-xl font-bold text-white" data-i18n="about.whyBentopdf.noAccount.title">No Account Required
</h3>
<p class="text-gray-400 mt-2" data-i18n="about.whyBentopdf.noAccount.description">
Start using any tool immediately. We don't need your email, a
password, or any personal information. Your workflow should be
frictionless and anonymous.
@@ -157,8 +162,9 @@
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
<i data-lucide="code-2" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
<div>
<h3 class="text-xl font-bold text-white">Open Source Spirit</h3>
<p class="text-gray-400 mt-2">
<h3 class="text-xl font-bold text-white" data-i18n="about.whyBentopdf.openSource.title">Open Source Spirit
</h3>
<p class="text-gray-400 mt-2" data-i18n="about.whyBentopdf.openSource.description">
Built with transparency in mind. We leverage incredible
open-source libraries like PDF-lib and PDF.js, and believe in
the community-driven effort to make powerful tools accessible to
@@ -172,16 +178,17 @@
<div class="section-divider"></div>
<section id="cta-section" class="text-center py-16">
<h2 class="text-3xl font-bold text-white mb-4">
<h2 class="text-3xl font-bold text-white mb-4" data-i18n="about.cta.title">
Ready to get started?
</h2>
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto" data-i18n="about.cta.description">
Join thousands of users who trust Bentopdf for their daily document
needs. Experience the difference that privacy and performance can
make.
</p>
<a href="index.html#tools-header"
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">
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"
data-i18n="about.cta.button">
Explore All Tools
</a>
</section>
@@ -195,46 +202,47 @@
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
<span class="text-xl font-bold text-white">BentoPDF</span>
</div>
<p class="text-gray-400 text-sm">
<p class="text-gray-400 text-sm" data-i18n="footer.copyright">
&copy; 2025 BentoPDF. All rights reserved.
</p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span>
<span data-i18n="footer.version">Version</span> <span id="app-version"></span>
</p>
</div>
<div>
<h3 class="font-bold text-white mb-4">Company</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.company">Company</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
<a href="./about.html" class="hover:text-indigo-400" data-i18n="footer.aboutUs">About Us</a>
</li>
<li>
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
<a href="./faq.html" class="hover:text-indigo-400" data-i18n="footer.faqLink">FAQ</a>
</li>
<li>
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
<a href="./contact.html" class="hover:text-indigo-400" data-i18n="footer.contactUs">Contact Us</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Legal</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.legal">Legal</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
<a href="./licensing.html" class="hover:text-indigo-400" data-i18n="nav.licensing">Licensing</a>
</li>
<li>
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
<a href="./terms.html" class="hover:text-indigo-400" data-i18n="footer.termsAndConditions">Terms and
Conditions</a>
</li>
<li>
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
<a href="./privacy.html" class="hover:text-indigo-400" data-i18n="footer.privacyPolicy">Privacy Policy</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Follow Us</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.followUs">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">
@@ -244,7 +252,7 @@
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer"
<a href="https://discord.gg/Bgq3Ay3f2w" 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
@@ -273,6 +281,7 @@
<script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body>

View File

@@ -5,6 +5,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Us - BentoPDF</title>
<link rel="alternate" hreflang="en" href="/en/contact.html" />
<link rel="alternate" hreflang="de" href="/de/contact.html" />
<link rel="alternate" hreflang="x-default" href="/en/contact.html" />
<link href="/src/css/styles.css" rel="stylesheet" />
</head>
@@ -24,11 +27,11 @@
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
<a href="index.html" class="nav-link" data-i18n="nav.home">Home</a>
<a href="./about.html" class="nav-link" data-i18n="nav.about">About</a>
<a href="./contact.html" class="nav-link" data-i18n="nav.contact">Contact</a>
<a href="./licensing.html" class="nav-link" data-i18n="nav.licensing">Licensing</a>
<a href="index.html#tools-header" class="nav-link" data-i18n="nav.allTools">All Tools</a>
</div>
<!-- Mobile Hamburger Button -->
@@ -55,21 +58,21 @@
<!-- 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="./licensing.html" class="mobile-nav-link">Licensing</a>
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
<a href="index.html" class="mobile-nav-link" data-i18n="nav.home">Home</a>
<a href="./about.html" class="mobile-nav-link" data-i18n="nav.about">About</a>
<a href="./contact.html" class="mobile-nav-link" data-i18n="nav.contact">Contact</a>
<a href="./licensing.html" class="mobile-nav-link" data-i18n="nav.licensing">Licensing</a>
<a href="index.html#tools-header" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
</div>
</div>
</nav>
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
<section id="contact-hero" class="text-center py-16 md:py-24">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4" data-i18n="contact.title">
Get in Touch
</h1>
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto" data-i18n="contact.subtitle">
We'd love to hear from you. Whether you have a question, feedback, or
a feature request, please don't hesitate to reach out.
</p>
@@ -77,7 +80,7 @@
<div class="max-w-2xl mx-auto text-center py-8">
<p class="text-lg text-gray-400">
You can reach us directly by email at:
<span data-i18n="contact.email">You can reach us directly by email at:</span>
<a href="mailto:contact@bentopdf.com"
class="text-indigo-400 underline hover:text-indigo-300">contact@bentopdf.com</a>
</p>
@@ -92,46 +95,47 @@
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
<span class="text-xl font-bold text-white">BentoPDF</span>
</div>
<p class="text-gray-400 text-sm">
<p class="text-gray-400 text-sm" data-i18n="footer.copyright">
&copy; 2025 BentoPDF. All rights reserved.
</p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span>
<span data-i18n="footer.version">Version</span> <span id="app-version"></span>
</p>
</div>
<div>
<h3 class="font-bold text-white mb-4">Company</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.company">Company</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
<a href="./about.html" class="hover:text-indigo-400" data-i18n="footer.aboutUs">About Us</a>
</li>
<li>
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
<a href="./faq.html" class="hover:text-indigo-400" data-i18n="footer.faqLink">FAQ</a>
</li>
<li>
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
<a href="./contact.html" class="hover:text-indigo-400" data-i18n="footer.contactUs">Contact Us</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Legal</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.legal">Legal</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
<a href="./licensing.html" class="hover:text-indigo-400" data-i18n="nav.licensing">Licensing</a>
</li>
<li>
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
<a href="./terms.html" class="hover:text-indigo-400" data-i18n="footer.termsAndConditions">Terms and
Conditions</a>
</li>
<li>
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
<a href="./privacy.html" class="hover:text-indigo-400" data-i18n="footer.privacyPolicy">Privacy Policy</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Follow Us</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.followUs">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">
@@ -141,7 +145,7 @@
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer"
<a href="https://discord.gg/Bgq3Ay3f2w" 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
@@ -170,6 +174,7 @@
<script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body>

View File

@@ -288,7 +288,7 @@
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer"
<a href="https://discord.gg/Bgq3Ay3f2w" 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

View File

@@ -5,10 +5,13 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BentoPDF - The Privacy First PDF Toolkit</title>
<!-- i18n: hreflang tags for SEO -->
<link rel="alternate" hreflang="en" href="/en/" />
<link rel="alternate" hreflang="de" href="/de/" />
<link rel="alternate" hreflang="x-default" href="/en/" />
<link rel="icon" type="image/svg+xml" href="images/favicon.svg" />
<link rel="icon" type="image/png" href="images/favicon.png" />
<link rel="apple-touch-icon" href="images/favicon.png" />
<link rel="icon" href="favicon.ico" sizes="any" />
<link href="src/css/styles.css" rel="stylesheet" />
</head>
@@ -25,11 +28,11 @@
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
<a href="index.html" class="nav-link" data-i18n="nav.home">Home</a>
<a href="./about.html" class="nav-link" data-i18n="nav.about">About</a>
<a href="./contact.html" class="nav-link" data-i18n="nav.contact">Contact</a>
<a href="./licensing.html" class="nav-link" data-i18n="nav.licensing">Licensing</a>
<a href="index.html#tools-header" class="nav-link" data-i18n="nav.allTools">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
@@ -65,7 +68,7 @@
<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>
<span class="sr-only" data-i18n="nav.openMainMenu">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">
@@ -84,11 +87,11 @@
<!-- 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="./licensing.html" class="mobile-nav-link">Licensing</a>
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
<a href="index.html" class="mobile-nav-link" data-i18n="nav.home">Home</a>
<a href="./about.html" class="mobile-nav-link" data-i18n="nav.about">About</a>
<a href="./contact.html" class="mobile-nav-link" data-i18n="nav.contact">Contact</a>
<a href="./licensing.html" class="mobile-nav-link" data-i18n="nav.licensing">Licensing</a>
<a href="index.html#tools-header" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
</div>
</div>
</nav>
@@ -96,11 +99,13 @@
<section id="hero-section" class="text-center py-20 relative">
<div class="bg-grid-pattern"></div>
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
The <span
<span data-i18n="hero.title">The</span> <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
<span data-i18n="hero.pdfToolkit">PDF Toolkit</span> <i data-lucide="briefcase"
class="w-8 h-8 md:w-12 md:h-12 transform -rotate-4"></i> </span> <span
data-i18n="hero.builtForPrivacy">built
for
privacy</span><span
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
</h1>
<!-- <p class="text-lg text-gray-400 mb-12">Fast, Secure and Forever Free.</p> -->
@@ -108,24 +113,24 @@
<span
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium">
<i data-lucide="check-circle" class="w-4 h-4 text-gray-400"></i>
No Signups
<span data-i18n="hero.noSignups">No Signups</span>
</span>
<span
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium">
<i data-lucide="check-circle" class="w-4 h-4 text-gray-400"></i>
Unlimited Use
<span data-i18n="hero.unlimitedUse">Unlimited Use</span>
</span>
<span
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-gray-800/40 border border-gray-700/50 text-gray-400 text-sm font-medium">
<i data-lucide="check-circle" class="w-4 h-4 text-gray-400"></i>
Works Offline
<span data-i18n="hero.worksOffline">Works Offline</span>
</span>
</div>
<div class="flex flex-col items-center gap-4">
<a href="#tools-header"
class="inline-flex items-center gap-3 pl-8 pr-2 py-2 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">
Start Using Now
<span data-i18n="hero.startUsing">Start Using Now</span>
<span class="bg-white text-indigo-600 rounded-full p-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
@@ -141,7 +146,8 @@
<!-- 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>
<p class="text-center text-gray-500 text-sm font-medium mb-8" data-i18n="usedBy.title">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">
@@ -200,58 +206,61 @@
<section id="features-section" class="py-20">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
Why <span class="marker-slanted">BentoPDF?</span>
<span data-i18n="features.title"></span>Why</span> <span class="marker-slanted"
data-i18n="features.bentoPdf">BentoPDF?</span>
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">No Signup</h3>
<h3 class="text-xl font-bold text-white" data-i18n="features.noSignup.title">No Signup</h3>
</div>
<p class="text-gray-400 pl-14">
<p class="text-gray-400 pl-14" data-i18n="features.noSignup.description">
Start instantly, no accounts or emails.
</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="shield" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">No Uploads</h3>
<h3 class="text-xl font-bold text-white" data-i18n="features.noUploads.title">No Uploads</h3>
</div>
<p class="text-gray-400 pl-14">
<p class="text-gray-400 pl-14" data-i18n="features.noUploads.description">
100% client-side, your files never leave your device.
</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">Forever Free</h3>
<h3 class="text-xl font-bold text-white" data-i18n="features.foreverFree.title">Forever Free</h3>
</div>
<p class="text-gray-400 pl-14">
<p class="text-gray-400 pl-14" data-i18n="features.foreverFree.description">
All tools, no trials, no paywalls.
</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="infinity" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">No Limits</h3>
<h3 class="text-xl font-bold text-white" data-i18n="features.noLimits.title">No Limits</h3>
</div>
<p class="text-gray-400 pl-14">
<p class="text-gray-400 pl-14" data-i18n="features.noLimits.description">
Use as much as you want, no hidden caps.
</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="layers" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">Batch Processing</h3>
<h3 class="text-xl font-bold text-white" data-i18n="features.batchProcessing.title">Batch Processing</h3>
</div>
<p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p>
<p class="text-gray-400 pl-14" data-i18n="features.batchProcessing.description">Handle unlimited PDFs in one
go.
</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">Lightning Fast</h3>
<h3 class="text-xl font-bold text-white" data-i18n="features.lightningFast.title">Lightning Fast</h3>
</div>
<p class="text-gray-400 pl-14">
<p class="text-gray-400 pl-14" data-i18n="features.lightningFast.description">
Process PDFs instantly, without waiting or delays.
</p>
</div>
@@ -262,9 +271,10 @@
<div id="tools-header" class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-3">
Get Started with <span class="marker-slanted ml-2"> Tools</span>
<span data-i18n="tools.title">Get Started with</span> <span class="marker-slanted ml-2"
data-i18n="tools.toolsLabel"> Tools</span>
</h2>
<p class="text-gray-400">Click a tool to open the file uploader</p>
<p class="text-gray-400" data-i18n="tools.subtitle">Click a tool to open the file uploader</p>
</div>
<div id="grid-view">
@@ -275,7 +285,8 @@
</span>
<input type="text" id="search-bar"
class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Search for a tool (e.g., 'split', 'organize'...)" />
placeholder="Search for a tool (e.g., 'split', 'organize'...)"
data-i18n-placeholder="tools.searchPlaceholder" />
<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-2.5 py-1.5 rounded-md text-xs font-semibold text-gray-300 border border-gray-700 shadow-sm"></kbd>
@@ -296,7 +307,7 @@
<button id="back-to-grid"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer"> Back to Tools </span>
<span class="cursor-pointer" data-i18n="tools.backToTools"> Back to Tools </span>
</button>
<div id="tool-content"></div>
</div>
@@ -304,7 +315,7 @@
<div id="loader-modal" class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
<div class="solid-spinner"></div>
<p id="loader-text" class="text-white text-lg font-medium">
<p id="loader-text" class="text-white text-lg font-medium" data-i18n="loader.processing">
Processing...
</p>
</div>
@@ -367,7 +378,7 @@
</button>
<button id="preferences-tab-btn" class="flex-1 py-3 text-sm font-medium text-gray-300 hover:text-white">
<i data-lucide="sliders" class="inline-block w-4 h-4 mr-1"></i>
Preferences
<span data-i18n="settings.preferences">Preferences</span>
</button>
</div>
@@ -399,15 +410,17 @@
<!-- Preferences Tab Content -->
<div id="preferences-tab-content" class="hidden p-6 overflow-y-auto flex-grow">
<h4 class="text-lg font-semibold text-white mb-4">Display Preferences</h4>
<h4 class="text-lg font-semibold text-white mb-4" data-i18n="settings.displayPreferences">Display Preferences
</h4>
<div class="space-y-4">
<div class="flex items-center justify-between p-4 bg-gray-900 rounded-lg border border-gray-700">
<div class="flex-1">
<label for="full-width-toggle" class="text-sm font-medium text-gray-200 cursor-pointer">
<label for="full-width-toggle" class="text-sm font-medium text-gray-200 cursor-pointer"
data-i18n="settings.fullWidthMode">
Full Width Mode
</label>
<p class="text-xs text-gray-400 mt-1">
<p class="text-xs text-gray-400 mt-1" data-i18n="settings.fullWidthDescription">
Use the full screen width for all tools instead of a centered container
</p>
</div>
@@ -433,7 +446,8 @@
</button>
</div>
<div id="preferences-tab-footer" class="hidden w-full">
<p class="text-xs text-gray-400 text-center">Settings are automatically saved</p>
<p class="text-xs text-gray-400 text-center" data-i18n="settings.settingsAutoSaved">Settings are
automatically saved</p>
</div>
<button id="reset-shortcuts-btn"
class="text-red-400 hover:text-red-300 text-sm font-medium transition-colors">
@@ -479,21 +493,23 @@
<section id="security-compliance-section" class="py-20 hide-section">
<div class="mb-8 text-center">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance">
Your data never leaves your device
<span data-i18n="compliance.title"></span>Your data never leaves your device</span>
<span class="inline-flex items-center gap-2">
<i data-lucide="shield" class="w-8 h-8 md:w-10 md:h-10 text-indigo-400 bg-indigo-900 rounded-lg p-1.5"></i>
We keep
<span data-i18n="compliance.weKeep">We keep</span>
</span>
<br class="hidden sm:block" />
<span
class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold">
class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold"
data-i18n="compliance.yourInfoSafe">
your information safe
</span>
by following global security standards.
<span data-i18n="compliance.byFollowingStandards">by following global security standards.</span>
</h2>
</div>
<div class="mb-16 text-center">
<span class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors">
<span class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors"
data-i18n="compliance.processingLocal">
All the processing happens locally on your device.
</span>
</div>
@@ -504,10 +520,11 @@
<div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
<img src="images/gdpr.svg" alt="GDPR compliance" class="w-full h-full" />
</div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
<h3 class="text-lg md:text-xl font-bold text-white mb-3" data-i18n="compliance.gdpr.title">
GDPR compliance
</h3>
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
data-i18n="compliance.gdpr.description">
Protects the personal data and privacy of individuals within the
European Union.
</p>
@@ -517,10 +534,11 @@
<div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
<img src="images/ccpa.svg" alt="CCPA compliance" class="w-full h-full" />
</div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
<h3 class="text-lg md:text-xl font-bold text-white mb-3" data-i18n="compliance.ccpa.title">
CCPA compliance
</h3>
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
data-i18n="compliance.ccpa.description">
Gives California residents rights over how their personal
information is collected, used, and shared.
</p>
@@ -530,10 +548,11 @@
<div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
<img src="images/hipaa.svg" alt="HIPAA compliance" class="w-full h-full" />
</div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3">
<h3 class="text-lg md:text-xl font-bold text-white mb-3" data-i18n="compliance.hipaa.title">
HIPAA compliance
</h3>
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs"
data-i18n="compliance.hipaa.description">
Sets safeguards for handling sensitive health information in the
United States healthcare system.
</p>
@@ -547,16 +566,17 @@
<section id="faq-accordion" class="space-y-4 hide-section">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8">
Frequently Asked <span class="marker-slanted">Questions</span>
<span data-i18n="faq.title">Frequently Asked</span> <span class="marker-slanted"
data-i18n="faq.questions">Questions</span>
</h2>
<!-- Existing FAQs here... -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Is BentoPDF really free?</span>
<span class="text-lg font-semibold text-white" data-i18n="faq.isFree.question">Is BentoPDF really free?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
<p class="p-6 pt-0 text-gray-400" data-i18n="faq.isFree.answer">
Yes, absolutely. All tools on BentoPDF are 100% free to use, with
no file limits, no sign-ups, and no watermarks. We believe
everyone deserves access to simple, powerful PDF tools without a
@@ -567,11 +587,12 @@
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Are my files secure? Where are they processed?</span>
<span class="text-lg font-semibold text-white" data-i18n="faq.areFilesSecure.question">Are my files secure?
Where are they processed?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
<p class="p-6 pt-0 text-gray-400" data-i18n="faq.areFilesSecure.answer">
Your files are as secure as possible because they **never leave
your computer**. All processing happens directly in your web
browser (client-side). We never upload your files to a server, so
@@ -582,11 +603,12 @@
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Does it work on Mac, Windows, and Mobile?</span>
<span class="text-lg font-semibold text-white" data-i18n="faq.platforms.question">Does it work on Mac,
Windows, and Mobile?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
<p class="p-6 pt-0 text-gray-400" data-i18n="faq.platforms.answer">
Yes! Since BentoPDF runs entirely in your browser, it works on any
operating system with a modern web browser, including Windows,
macOS, Linux, iOS, and Android.
@@ -597,11 +619,12 @@
<!-- New FAQ: GDPR Compliance -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Is BentoPDF GDPR compliant?</span>
<span class="text-lg font-semibold text-white" data-i18n="faq.gdprCompliant.question">Is BentoPDF GDPR
compliant?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
<p class="p-6 pt-0 text-gray-400" data-i18n="faq.gdprCompliant.answer">
Yes. BentoPDF is fully GDPR compliant. Since all file processing
happens locally in your browser and we never collect or transmit
your files to any server, we have no access to your data. This
@@ -613,11 +636,12 @@
<!-- New FAQ: Data Storage -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Do you store or track any of my files?</span>
<span class="text-lg font-semibold text-white" data-i18n="faq.dataStorage.question">Do you store or track any
of my files?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
<p class="p-6 pt-0 text-gray-400" data-i18n="faq.dataStorage.answer">
No. We never store, track, or log your files. Everything you do on
BentoPDF happens in your browser memory and disappears once you
close the page. There are no uploads, no history logs, and no
@@ -629,11 +653,12 @@
<!-- New FAQ: Privacy -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">What makes BentoPDF different from other PDF tools?</span>
<span class="text-lg font-semibold text-white" data-i18n="faq.different.question">What makes BentoPDF
different from other PDF tools?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
<p class="p-6 pt-0 text-gray-400" data-i18n="faq.different.answer">
Most PDF tools upload your files to a server for processing.
BentoPDF never does that. We use secure, modern web technology to
process your files directly in your browser. This means faster
@@ -645,11 +670,12 @@
<!-- New FAQ: Browser-Based -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">How does browser-based processing keep me safe?</span>
<span class="text-lg font-semibold text-white" data-i18n="faq.browserBased.question">How does browser-based
processing keep me safe?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
<p class="p-6 pt-0 text-gray-400" data-i18n="faq.browserBased.answer">
By running entirely inside your browser, BentoPDF ensures that
your files never leave your device. This eliminates the risks of
server hacks, data breaches, or unauthorized access. Your files
@@ -661,11 +687,12 @@
<!-- New FAQ: Analytics -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Do you use cookies or analytics to track me?</span>
<span class="text-lg font-semibold text-white" data-i18n="faq.analytics.question">Do you use cookies or
analytics to track me?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
<p class="p-6 pt-0 text-gray-400" data-i18n="faq.analytics.answer">
We care about your privacy. BentoPDF does not track personal
information. We use
<a href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank"
@@ -683,7 +710,8 @@
<section id="testimonials-section" class="py-20 hide-section">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
What Our <span class="marker-slanted">Users</span> Say
<span data-i18n="testimonials.title">What Our</span> <span class="marker-slanted"
data-i18n="testimonials.users">Users</span> <span data-i18n="testimonials.say">Say</span>
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
<div class="testimonial-card">
@@ -783,10 +811,10 @@
<div class="section-divider"></div>
<section id="support-section" class="py-20">
<div class="max-w-4xl mx-auto text-center bg-gray-800 p-8 md:p-12 rounded-xl border border-gray-700 shadow-2xl">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4" data-i18n="support.title">
Like My Work?
</h2>
<p class="text-gray-400 mb-8 max-w-2xl mx-auto">
<p class="text-gray-400 mb-8 max-w-2xl mx-auto" data-i18n="support.description">
BentoPDF is a passion project, built to provide a free, private, and
powerful PDF toolkit for everyone. If you find it useful, consider
supporting its development. Every coffee helps!
@@ -795,7 +823,7 @@
<a href="https://ko-fi.com/alio01" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center gap-3 px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5">
<i data-lucide="coffee" class="w-7 h-7"></i>
<span>Buy Me a Coffee</span>
<span data-i18n="support.buyMeCoffee">Buy Me a Coffee</span>
</a>
</div>
</section>
@@ -818,46 +846,47 @@
<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">
<p class="text-gray-400 text-sm" data-i18n="footer.copyright">
&copy; 2025 BentoPDF. All rights reserved.
</p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span>
<span data-i18n="footer.version">Version</span> <span id="app-version"></span>
</p>
</div>
<div>
<h3 class="font-bold text-white mb-4">Company</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.company">Company</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
<a href="./about.html" class="hover:text-indigo-400" data-i18n="footer.aboutUs">About Us</a>
</li>
<li>
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
<a href="./faq.html" class="hover:text-indigo-400" data-i18n="footer.faqLink">FAQ</a>
</li>
<li>
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
<a href="./contact.html" class="hover:text-indigo-400" data-i18n="footer.contactUs">Contact Us</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Legal</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.legal">Legal</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
<a href="./licensing.html" class="hover:text-indigo-400" data-i18n="nav.licensing">Licensing</a>
</li>
<li>
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
<a href="./terms.html" class="hover:text-indigo-400" data-i18n="footer.termsAndConditions">Terms and
Conditions</a>
</li>
<li>
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
<a href="./privacy.html" class="hover:text-indigo-400" data-i18n="footer.privacyPolicy">Privacy Policy</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Follow Us</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.followUs">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">
@@ -867,7 +896,7 @@
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer"
<a href="https://discord.gg/Bgq3Ay3f2w" 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

View File

@@ -5,6 +5,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Licensing - BentoPDF</title>
<link rel="alternate" hreflang="en" href="/en/licensing.html" />
<link rel="alternate" hreflang="de" href="/de/licensing.html" />
<link rel="alternate" hreflang="x-default" href="/en/licensing.html" />
<link href="/src/css/styles.css" rel="stylesheet" />
</head>
@@ -24,11 +27,11 @@
<!-- Desktop Navigation -->
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="./licensing.html" class="nav-link">Licensing</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
<a href="index.html" class="nav-link" data-i18n="nav.home">Home</a>
<a href="./about.html" class="nav-link" data-i18n="nav.about">About</a>
<a href="./contact.html" class="nav-link" data-i18n="nav.contact">Contact</a>
<a href="./licensing.html" class="nav-link" data-i18n="nav.licensing">Licensing</a>
<a href="index.html#tools-header" class="nav-link" data-i18n="nav.allTools">All Tools</a>
</div>
<!-- Mobile Hamburger Button -->
@@ -57,11 +60,11 @@
<!-- 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="./licensing.html" class="mobile-nav-link">Licensing</a>
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
<a href="index.html" class="mobile-nav-link" data-i18n="nav.home">Home</a>
<a href="./about.html" class="mobile-nav-link" data-i18n="nav.about">About</a>
<a href="./contact.html" class="mobile-nav-link" data-i18n="nav.contact">Contact</a>
<a href="./licensing.html" class="mobile-nav-link" data-i18n="nav.licensing">Licensing</a>
<a href="index.html#tools-header" class="mobile-nav-link" data-i18n="nav.allTools">All Tools</a>
</div>
</div>
</nav>
@@ -69,10 +72,10 @@
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
<section id="licensing-hero" class="text-center py-16 md:py-24">
<h1 class="text-3xl md:text-6xl font-bold text-white mb-4">
Licensing for
<span data-i18n="licensing.title">Licensing for</span>
<span class="marker-slanted">BentoPDF</span>
</h1>
<p class="text-lg md:text-xl text-gray-400">
<p class="text-lg md:text-xl text-gray-400" data-i18n="licensing.subtitle">
Choose the license that fits your needs.
</p>
</section>
@@ -601,46 +604,50 @@
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3" />
<span class="text-xl font-bold text-white">BentoPDF</span>
</div>
<p class="text-gray-400 text-sm">
<p class="text-gray-400 text-sm" data-i18n="footer.copyright">
&copy; 2025 BentoPDF. All rights reserved.
</p>
<p class="text-gray-500 text-xs mt-2">
Version <span id="app-version"></span>
<span data-i18n="footer.version">Version</span> <span id="app-version"></span>
</p>
</div>
<div>
<h3 class="font-bold text-white mb-4">Company</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.company">Company</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="./about.html" class="hover:text-indigo-400">About Us</a>
<a href="./about.html" class="hover:text-indigo-400" data-i18n="footer.aboutUs">About Us</a>
</li>
<li>
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
<a href="./faq.html" class="hover:text-indigo-400" data-i18n="footer.faqLink">FAQ</a>
</li>
<li>
<a href="./contact.html" class="hover:text-indigo-400">Contact Us</a>
<a href="./contact.html" class="hover:text-indigo-400" data-i18n="footer.contactUs">Contact
Us</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Legal</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.legal">Legal</h3>
<ul class="space-y-2 text-gray-400">
<li>
<a href="./licensing.html" class="hover:text-indigo-400">Licensing</a>
<a href="./licensing.html" class="hover:text-indigo-400"
data-i18n="nav.licensing">Licensing</a>
</li>
<li>
<a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a>
<a href="./terms.html" class="hover:text-indigo-400"
data-i18n="footer.termsAndConditions">Terms and Conditions</a>
</li>
<li>
<a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a>
<a href="./privacy.html" class="hover:text-indigo-400"
data-i18n="footer.privacyPolicy">Privacy Policy</a>
</li>
</ul>
</div>
<div>
<h3 class="font-bold text-white mb-4">Follow Us</h3>
<h3 class="font-bold text-white mb-4" data-i18n="footer.followUs">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">
@@ -650,7 +657,7 @@
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer"
<a href="https://discord.gg/Bgq3Ay3f2w" 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
@@ -680,6 +687,7 @@
<script type="module" src="src/js/utils/lucide-init.ts"></script>
<script type="module" src="src/version.ts"></script>
<script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/faq.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script>
</body>

67
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "bento-pdf",
"version": "1.10.0",
"version": "1.10.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bento-pdf",
"version": "1.10.0",
"version": "1.10.5",
"license": "Apache-2.0",
"dependencies": {
"@fontsource/cedarville-cursive": "^5.2.7",
@@ -24,6 +24,9 @@
"cropperjs": "^1.6.1",
"heic2any": "^0.0.4",
"html2canvas": "^1.4.1",
"i18next": "^25.7.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"javascript-obfuscator": "^4.1.1",
"jspdf": "^3.0.3",
"jszip": "^3.10.1",
@@ -6031,6 +6034,15 @@
"integrity": "sha512-F4wsi+XkDHCOMrHMYjrTEE4QBOrsHHN5/2VsVAaRq8P7E5z7xQpT75S+f/9WikmBEailas3+yo+6zPIomW+NOA==",
"license": "MIT"
},
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -7949,6 +7961,55 @@
"node": ">= 14"
}
},
"node_modules/i18next": {
"version": "25.7.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.2.tgz",
"integrity": "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-http-backend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
"license": "MIT",
"dependencies": {
"cross-fetch": "4.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -12500,7 +12561,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View File

@@ -57,6 +57,9 @@
"cropperjs": "^1.6.1",
"heic2any": "^0.0.4",
"html2canvas": "^1.4.1",
"i18next": "^25.7.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"javascript-obfuscator": "^4.1.1",
"jspdf": "^3.0.3",
"jszip": "^3.10.1",

View File

@@ -248,7 +248,7 @@
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/AP2Y97juZT" target="_blank" rel="noopener noreferrer"
<a href="https://discord.gg/Bgq3Ay3f2w" 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

View File

@@ -0,0 +1,275 @@
{
"nav": {
"home": "Startseite",
"about": "Über uns",
"contact": "Kontakt",
"licensing": "Lizenzierung",
"allTools": "Alle Werkzeuge",
"openMainMenu": "Hauptmenü öffnen",
"language": "Sprache"
},
"hero": {
"title": "Das",
"pdfToolkit": "PDF-Toolkit",
"builtForPrivacy": "für maximale Privatsphäre",
"noSignups": "Keine Anmeldung",
"unlimitedUse": "Unbegrenzte Nutzung",
"worksOffline": "Funktioniert offline",
"startUsing": "Jetzt starten"
},
"usedBy": {
"title": "Verwendet von Unternehmen und Mitarbeitern bei"
},
"features": {
"title": "Warum",
"bentoPdf": "BentoPDF?",
"noSignup": {
"title": "Keine Anmeldung",
"description": "Sofort starten, keine Konten oder E-Mails erforderlich."
},
"noUploads": {
"title": "Kein Upload",
"description": "100% clientseitig, Ihre Dateien verlassen nie Ihr Gerät."
},
"foreverFree": {
"title": "Für immer kostenlos",
"description": "Alle Werkzeuge, keine Testversionen, keine Bezahlschranken."
},
"noLimits": {
"title": "Keine Limits",
"description": "Nutzen Sie so viel Sie wollen, ohne versteckte Grenzen."
},
"batchProcessing": {
"title": "Stapelverarbeitung",
"description": "Verarbeiten Sie unbegrenzt viele PDFs auf einmal."
},
"lightningFast": {
"title": "Blitzschnell",
"description": "PDFs sofort verarbeiten, ohne Wartezeiten."
}
},
"tools": {
"title": "Starten Sie mit",
"toolsLabel": "Werkzeugen",
"subtitle": "Klicken Sie auf ein Werkzeug, um den Datei-Uploader zu öffnen",
"searchPlaceholder": "Werkzeug suchen (z.B. 'teilen', 'organisieren'...)",
"backToTools": "Zurück zu den Werkzeugen"
},
"upload": {
"clickToSelect": "Klicken Sie, um eine Datei auszuwählen",
"orDragAndDrop": "oder per Drag & Drop",
"pdfOrImages": "PDFs oder Bilder",
"filesNeverLeave": "Ihre Dateien verlassen nie Ihr Gerät.",
"addMore": "Weitere Dateien hinzufügen",
"clearAll": "Alle löschen"
},
"loader": {
"processing": "Verarbeitung..."
},
"alert": {
"title": "Hinweis",
"ok": "OK"
},
"preview": {
"title": "Dokumentvorschau",
"downloadAsPdf": "Als PDF herunterladen",
"close": "Schließen"
},
"settings": {
"title": "Einstellungen",
"shortcuts": "Tastenkürzel",
"preferences": "Voreinstellungen",
"displayPreferences": "Anzeige-Einstellungen",
"searchShortcuts": "Tastenkürzel suchen...",
"shortcutsInfo": "Halten Sie Tasten gedrückt, um ein Kürzel festzulegen. Änderungen werden automatisch gespeichert.",
"shortcutsWarning": "⚠️ Vermeiden Sie gängige Browser-Tastenkürzel (Strg+W, Strg+T, Strg+N usw.), da diese möglicherweise nicht zuverlässig funktionieren.",
"import": "Importieren",
"export": "Exportieren",
"resetToDefaults": "Auf Standard zurücksetzen",
"fullWidthMode": "Vollbreite-Modus",
"fullWidthDescription": "Verwenden Sie die volle Bildschirmbreite für alle Werkzeuge anstelle eines zentrierten Containers",
"settingsAutoSaved": "Einstellungen werden automatisch gespeichert",
"clickToSet": "Klicken zum Festlegen",
"pressKeys": "Tasten drücken...",
"warnings": {
"alreadyInUse": "Tastenkombination bereits vergeben",
"assignedTo": "ist bereits zugewiesen an:",
"chooseDifferent": "Bitte wählen Sie eine andere Tastenkombination.",
"reserved": "Warnung: Reservierte Tastenkombination",
"commonlyUsed": "wird häufig verwendet für:",
"unreliable": "Diese Tastenkombination funktioniert möglicherweise nicht zuverlässig oder könnte mit Browser-/Systemfunktionen in Konflikt geraten.",
"useAnyway": "Möchten Sie sie trotzdem verwenden?",
"resetTitle": "Tastenkombinationen zurücksetzen",
"resetMessage": "Sind Sie sicher, dass Sie alle Tastenkombinationen auf die Standardwerte zurücksetzen möchten?<br><br>Diese Aktion kann nicht rückgängig gemacht werden.",
"importSuccessTitle": "Import erfolgreich",
"importSuccessMessage": "Tastenkombinationen erfolgreich importiert!",
"importFailTitle": "Import fehlgeschlagen",
"importFailMessage": "Fehler beim Importieren der Tastenkombinationen. Ungültiges Dateiformat."
}
},
"warning": {
"title": "Warnung",
"cancel": "Abbrechen",
"proceed": "Fortfahren"
},
"compliance": {
"title": "Ihre Daten verlassen nie Ihr Gerät",
"weKeep": "Wir schützen",
"yourInfoSafe": "Ihre Informationen",
"byFollowingStandards": "nach globalen Sicherheitsstandards.",
"processingLocal": "Die gesamte Verarbeitung erfolgt lokal auf Ihrem Gerät.",
"gdpr": {
"title": "DSGVO-konform",
"description": "Schützt die personenbezogenen Daten und die Privatsphäre von Personen innerhalb der Europäischen Union."
},
"ccpa": {
"title": "CCPA-konform",
"description": "Gibt Einwohnern Kaliforniens Rechte darüber, wie ihre persönlichen Daten gesammelt, verwendet und weitergegeben werden."
},
"hipaa": {
"title": "HIPAA-konform",
"description": "Legt Schutzmaßnahmen für den Umgang mit sensiblen Gesundheitsinformationen im US-Gesundheitssystem fest."
}
},
"faq": {
"title": "Häufig gestellte",
"questions": "Fragen",
"isFree": {
"question": "Ist BentoPDF wirklich kostenlos?",
"answer": "Ja, absolut. Alle Werkzeuge auf BentoPDF sind zu 100% kostenlos nutzbar, ohne Dateilimits, ohne Anmeldung und ohne Wasserzeichen. Wir glauben, dass jeder Zugang zu einfachen, leistungsstarken PDF-Werkzeugen verdient, ohne Bezahlschranke."
},
"areFilesSecure": {
"question": "Sind meine Dateien sicher? Wo werden sie verarbeitet?",
"answer": "Ihre Dateien sind so sicher wie möglich, da sie nie Ihren Computer verlassen. Die gesamte Verarbeitung erfolgt direkt in Ihrem Webbrowser (clientseitig). Wir laden Ihre Dateien nie auf einen Server hoch, sodass Sie die vollständige Privatsphäre und Kontrolle über Ihre Dokumente behalten."
},
"platforms": {
"question": "Funktioniert es auf Mac, Windows und Mobilgeräten?",
"answer": "Ja! Da BentoPDF vollständig in Ihrem Browser läuft, funktioniert es auf jedem Betriebssystem mit einem modernen Webbrowser, einschließlich Windows, macOS, Linux, iOS und Android."
},
"gdprCompliant": {
"question": "Ist BentoPDF DSGVO-konform?",
"answer": "Ja. BentoPDF ist vollständig DSGVO-konform. Da die gesamte Dateiverarbeitung lokal in Ihrem Browser erfolgt und wir Ihre Dateien nie sammeln oder übertragen, haben wir keinen Zugang zu Ihren Daten. Dies stellt sicher, dass Sie immer die Kontrolle über Ihre Dokumente haben."
},
"dataStorage": {
"question": "Speichern oder verfolgen Sie meine Dateien?",
"answer": "Nein. Wir speichern, verfolgen oder protokollieren Ihre Dateien niemals. Alles, was Sie auf BentoPDF tun, geschieht im Speicher Ihres Browsers und verschwindet, sobald Sie die Seite schließen. Es gibt keine Uploads, keine Verlaufsprotokolle und keine Server."
},
"different": {
"question": "Was unterscheidet BentoPDF von anderen PDF-Werkzeugen?",
"answer": "Die meisten PDF-Werkzeuge laden Ihre Dateien zur Verarbeitung auf einen Server hoch. BentoPDF tut das nie. Wir verwenden sichere, moderne Webtechnologie, um Ihre Dateien direkt in Ihrem Browser zu verarbeiten. Das bedeutet schnellere Leistung, stärkere Privatsphäre und vollständige Sicherheit."
},
"browserBased": {
"question": "Wie schützt mich die browserbasierte Verarbeitung?",
"answer": "Durch die vollständige Ausführung in Ihrem Browser stellt BentoPDF sicher, dass Ihre Dateien nie Ihr Gerät verlassen. Dies eliminiert die Risiken von Server-Hacks, Datenschutzverletzungen oder unbefugtem Zugriff. Ihre Dateien bleiben Ihre — immer."
},
"analytics": {
"question": "Verwenden Sie Cookies oder Analysen, um mich zu verfolgen?",
"answer": "Uns liegt Ihre Privatsphäre am Herzen. BentoPDF verfolgt keine persönlichen Informationen. Wir verwenden Simple Analytics ausschließlich, um anonyme Besucherzahlen zu sehen. Das bedeutet, wir können wissen, wie viele Benutzer unsere Seite besuchen, aber wir wissen nie, wer Sie sind. Simple Analytics ist vollständig DSGVO-konform und respektiert Ihre Privatsphäre."
}
},
"testimonials": {
"title": "Was unsere",
"users": "Nutzer",
"say": "sagen"
},
"support": {
"title": "Gefällt Ihnen meine Arbeit?",
"description": "BentoPDF ist ein Leidenschaftsprojekt, entwickelt um ein kostenloses, privates und leistungsstarkes PDF-Toolkit für alle bereitzustellen. Wenn Sie es nützlich finden, erwägen Sie, die Entwicklung zu unterstützen. Jeder Kaffee hilft!",
"buyMeCoffee": "Kauf mir einen Kaffee"
},
"footer": {
"copyright": "© 2025 BentoPDF. Alle Rechte vorbehalten.",
"version": "Version",
"company": "Unternehmen",
"aboutUs": "Über uns",
"faqLink": "FAQ",
"contactUs": "Kontakt",
"legal": "Rechtliches",
"termsAndConditions": "Nutzungsbedingungen",
"privacyPolicy": "Datenschutzrichtlinie",
"followUs": "Folgen Sie uns"
},
"merge": {
"title": "PDFs zusammenführen",
"description": "Kombinieren Sie ganze Dateien oder wählen Sie bestimmte Seiten zum Zusammenführen in ein neues Dokument.",
"fileMode": "Datei-Modus",
"pageMode": "Seiten-Modus",
"howItWorks": "So funktioniert es:",
"fileModeInstructions": [
"Klicken und ziehen Sie das Symbol, um die Reihenfolge der Dateien zu ändern.",
"Im Feld \"Seiten\" für jede Datei können Sie Bereiche angeben (z.B. \"1-3, 5\"), um nur diese Seiten zusammenzuführen.",
"Lassen Sie das Feld \"Seiten\" leer, um alle Seiten dieser Datei einzuschließen."
],
"pageModeInstructions": [
"Alle Seiten Ihrer hochgeladenen PDFs werden unten angezeigt.",
"Ziehen Sie einfach die einzelnen Seitenvorschauen per Drag & Drop, um die gewünschte Reihenfolge für Ihre neue Datei zu erstellen."
],
"mergePdfs": "PDFs zusammenführen"
},
"common": {
"page": "Seite",
"pages": "Seiten",
"of": "von",
"download": "Herunterladen",
"cancel": "Abbrechen",
"save": "Speichern",
"delete": "Löschen",
"edit": "Bearbeiten",
"add": "Hinzufügen",
"remove": "Entfernen",
"loading": "Laden...",
"error": "Fehler",
"success": "Erfolg",
"file": "Datei",
"files": "Dateien"
},
"about": {
"hero": {
"title": "Wir glauben PDF-Werkzeuge sollten",
"subtitle": "schnell, privat und kostenlos sein.",
"noCompromises": "Ohne Kompromisse."
},
"mission": {
"title": "Unsere Mission",
"description": "Die umfassendste PDF-Toolbox bereitzustellen, die Ihre Privatsphäre respektiert und niemals eine Bezahlung verlangt. Wir glauben, dass wichtige Dokumentenwerkzeuge für jeden, überall und ohne Barrieren zugänglich sein sollten."
},
"philosophy": {
"label": "Unsere Kernphilosophie",
"title": "Privatsphäre zuerst. Immer.",
"description": "In einer Zeit, in der Daten eine Ware sind, gehen wir einen anderen Weg. Die gesamte Verarbeitung für Bentopdf-Werkzeuge erfolgt lokal in Ihrem Browser. Das bedeutet, Ihre Dateien berühren niemals unsere Server, wir sehen Ihre Dokumente nie und verfolgen nicht, was Sie tun. Ihre Dokumente bleiben vollständig und unmissverständlich privat. Es ist nicht nur eine Funktion; es ist unser Fundament."
},
"whyBentopdf": {
"title": "Warum",
"speed": {
"title": "Für Geschwindigkeit gebaut",
"description": "Kein Warten auf Uploads oder Downloads zu einem Server. Durch die Verarbeitung von Dateien direkt in Ihrem Browser mit modernen Webtechnologien wie WebAssembly bieten wir unvergleichliche Geschwindigkeit für alle unsere Werkzeuge."
},
"free": {
"title": "Komplett kostenlos",
"description": "Keine Testversionen, keine Abonnements, keine versteckten Gebühren und keine \"Premium\"-Funktionen als Geiseln. Wir glauben, leistungsstarke PDF-Werkzeuge sollten ein öffentliches Gut sein, kein Profitcenter."
},
"noAccount": {
"title": "Kein Konto erforderlich",
"description": "Beginnen Sie sofort mit der Nutzung eines beliebigen Werkzeugs. Wir brauchen weder Ihre E-Mail noch ein Passwort oder persönliche Informationen. Ihr Workflow sollte reibungslos und anonym sein."
},
"openSource": {
"title": "Open-Source-Geist",
"description": "Mit Transparenz im Blick entwickelt. Wir nutzen großartige Open-Source-Bibliotheken wie PDF-lib und PDF.js und glauben an die gemeinschaftsgetriebene Bemühung, leistungsstarke Werkzeuge für alle zugänglich zu machen."
}
},
"cta": {
"title": "Bereit loszulegen?",
"description": "Schließen Sie sich Tausenden von Benutzern an, die Bentopdf für ihre täglichen Dokumentenbedürfnisse vertrauen. Erleben Sie den Unterschied, den Privatsphäre und Leistung machen können.",
"button": "Alle Werkzeuge erkunden"
}
},
"contact": {
"title": "Kontakt aufnehmen",
"subtitle": "Wir freuen uns, von Ihnen zu hören. Ob Sie eine Frage, Feedback oder eine Funktionsanfrage haben, zögern Sie nicht, uns zu kontaktieren.",
"email": "Sie können uns direkt per E-Mail erreichen unter:"
},
"licensing": {
"title": "Lizenzierung für",
"subtitle": "Wählen Sie die Lizenz, die Ihren Anforderungen entspricht."
}
}

View File

@@ -0,0 +1,279 @@
{
"categories": {
"popularTools": "Beliebte Werkzeuge",
"editAnnotate": "Bearbeiten & Annotieren",
"convertToPdf": "In PDF konvertieren",
"convertFromPdf": "Aus PDF konvertieren",
"organizeManage": "Organisieren & Verwalten",
"optimizeRepair": "Optimieren & Reparieren",
"securePdf": "PDF sichern"
},
"pdfMultiTool": {
"name": "PDF Multi-Werkzeug",
"subtitle": "Zusammenführen, Teilen, Organisieren, Löschen, Drehen, Leere Seiten hinzufügen, Extrahieren und Duplizieren in einer einheitlichen Oberfläche."
},
"mergePdf": {
"name": "PDF zusammenführen",
"subtitle": "Mehrere PDFs zu einer Datei kombinieren. Lesezeichen werden beibehalten."
},
"splitPdf": {
"name": "PDF teilen",
"subtitle": "Einen Seitenbereich in eine neue PDF extrahieren."
},
"compressPdf": {
"name": "PDF komprimieren",
"subtitle": "Die Dateigröße Ihrer PDF reduzieren."
},
"pdfEditor": {
"name": "PDF-Editor",
"subtitle": "Annotieren, hervorheben, schwärzen, kommentieren, Formen/Bilder hinzufügen, suchen und PDFs anzeigen."
},
"jpgToPdf": {
"name": "JPG zu PDF",
"subtitle": "Eine PDF aus einem oder mehreren JPG-Bildern erstellen."
},
"signPdf": {
"name": "PDF unterschreiben",
"subtitle": "Zeichnen, tippen oder laden Sie Ihre Unterschrift hoch."
},
"cropPdf": {
"name": "PDF zuschneiden",
"subtitle": "Die Ränder jeder Seite in Ihrer PDF beschneiden."
},
"extractPages": {
"name": "Seiten extrahieren",
"subtitle": "Eine Auswahl von Seiten als neue Dateien speichern."
},
"duplicateOrganize": {
"name": "Duplizieren & Organisieren",
"subtitle": "Seiten duplizieren, neu anordnen und löschen."
},
"deletePages": {
"name": "Seiten löschen",
"subtitle": "Bestimmte Seiten aus Ihrem Dokument entfernen."
},
"editBookmarks": {
"name": "Lesezeichen bearbeiten",
"subtitle": "PDF-Lesezeichen hinzufügen, bearbeiten, importieren, löschen und extrahieren."
},
"tableOfContents": {
"name": "Inhaltsverzeichnis",
"subtitle": "Ein Inhaltsverzeichnis aus PDF-Lesezeichen generieren."
},
"pageNumbers": {
"name": "Seitenzahlen",
"subtitle": "Seitenzahlen in Ihr Dokument einfügen."
},
"addWatermark": {
"name": "Wasserzeichen hinzufügen",
"subtitle": "Text oder ein Bild über Ihre PDF-Seiten stempeln."
},
"headerFooter": {
"name": "Kopf- & Fußzeile",
"subtitle": "Text oben und unten auf Seiten hinzufügen."
},
"invertColors": {
"name": "Farben invertieren",
"subtitle": "Eine \"Dunkelmodus\"-Version Ihrer PDF erstellen."
},
"backgroundColor": {
"name": "Hintergrundfarbe",
"subtitle": "Die Hintergrundfarbe Ihrer PDF ändern."
},
"changeTextColor": {
"name": "Textfarbe ändern",
"subtitle": "Die Farbe des Textes in Ihrer PDF ändern."
},
"addStamps": {
"name": "Stempel hinzufügen",
"subtitle": "Bildstempel zu Ihrer PDF über die Annotations-Symbolleiste hinzufügen."
},
"removeAnnotations": {
"name": "Annotationen entfernen",
"subtitle": "Kommentare, Hervorhebungen und Links entfernen."
},
"pdfFormFiller": {
"name": "PDF-Formular ausfüllen",
"subtitle": "Formulare direkt im Browser ausfüllen. Unterstützt auch XFA-Formulare."
},
"createPdfForm": {
"name": "PDF-Formular erstellen",
"subtitle": "Ausfüllbare PDF-Formulare mit Drag-and-Drop-Textfeldern erstellen."
},
"removeBlankPages": {
"name": "Leere Seiten entfernen",
"subtitle": "Leere Seiten automatisch erkennen und löschen."
},
"imageToPdf": {
"name": "Bild zu PDF",
"subtitle": "JPG, PNG, WebP, BMP, TIFF, SVG, HEIC in PDF konvertieren."
},
"pngToPdf": {
"name": "PNG zu PDF",
"subtitle": "Eine PDF aus einem oder mehreren PNG-Bildern erstellen."
},
"webpToPdf": {
"name": "WebP zu PDF",
"subtitle": "Eine PDF aus einem oder mehreren WebP-Bildern erstellen."
},
"svgToPdf": {
"name": "SVG zu PDF",
"subtitle": "Eine PDF aus einem oder mehreren SVG-Bildern erstellen."
},
"bmpToPdf": {
"name": "BMP zu PDF",
"subtitle": "Eine PDF aus einem oder mehreren BMP-Bildern erstellen."
},
"heicToPdf": {
"name": "HEIC zu PDF",
"subtitle": "Eine PDF aus einem oder mehreren HEIC-Bildern erstellen."
},
"tiffToPdf": {
"name": "TIFF zu PDF",
"subtitle": "Eine PDF aus einem oder mehreren TIFF-Bildern erstellen."
},
"textToPdf": {
"name": "Text zu PDF",
"subtitle": "Eine Textdatei in eine PDF konvertieren."
},
"jsonToPdf": {
"name": "JSON zu PDF",
"subtitle": "JSON-Dateien in PDF-Format konvertieren."
},
"pdfToJpg": {
"name": "PDF zu JPG",
"subtitle": "Jede PDF-Seite in ein JPG-Bild konvertieren."
},
"pdfToPng": {
"name": "PDF zu PNG",
"subtitle": "Jede PDF-Seite in ein PNG-Bild konvertieren."
},
"pdfToWebp": {
"name": "PDF zu WebP",
"subtitle": "Jede PDF-Seite in ein WebP-Bild konvertieren."
},
"pdfToBmp": {
"name": "PDF zu BMP",
"subtitle": "Jede PDF-Seite in ein BMP-Bild konvertieren."
},
"pdfToTiff": {
"name": "PDF zu TIFF",
"subtitle": "Jede PDF-Seite in ein TIFF-Bild konvertieren."
},
"pdfToGreyscale": {
"name": "PDF zu Graustufen",
"subtitle": "Alle Farben in Schwarz-Weiß konvertieren."
},
"pdfToJson": {
"name": "PDF zu JSON",
"subtitle": "PDF-Dateien in JSON-Format konvertieren."
},
"ocrPdf": {
"name": "OCR PDF",
"subtitle": "Eine PDF durchsuchbar und kopierbar machen."
},
"alternateMix": {
"name": "Seiten abwechselnd mischen",
"subtitle": "PDFs durch abwechselnde Seiten aus jedem PDF zusammenführen. Lesezeichen werden beibehalten."
},
"addAttachments": {
"name": "Anhänge hinzufügen",
"subtitle": "Eine oder mehrere Dateien in Ihre PDF einbetten."
},
"extractAttachments": {
"name": "Anhänge extrahieren",
"subtitle": "Alle eingebetteten Dateien aus PDF(s) als ZIP extrahieren."
},
"editAttachments": {
"name": "Anhänge bearbeiten",
"subtitle": "Anhänge in Ihrer PDF anzeigen oder entfernen."
},
"dividePages": {
"name": "Seiten teilen",
"subtitle": "Seiten horizontal oder vertikal teilen."
},
"addBlankPage": {
"name": "Leere Seite hinzufügen",
"subtitle": "Eine leere Seite an beliebiger Stelle in Ihre PDF einfügen."
},
"reversePages": {
"name": "Seiten umkehren",
"subtitle": "Die Reihenfolge aller Seiten in Ihrem Dokument umkehren."
},
"rotatePdf": {
"name": "PDF drehen",
"subtitle": "Seiten in 90-Grad-Schritten drehen."
},
"nUpPdf": {
"name": "N-Up PDF",
"subtitle": "Mehrere Seiten auf einem einzigen Blatt anordnen."
},
"combineToSinglePage": {
"name": "Zu einer Seite kombinieren",
"subtitle": "Alle Seiten zu einem fortlaufenden Dokument zusammenfügen."
},
"viewMetadata": {
"name": "Metadaten anzeigen",
"subtitle": "Die versteckten Eigenschaften Ihrer PDF inspizieren."
},
"editMetadata": {
"name": "Metadaten bearbeiten",
"subtitle": "Autor, Titel und andere Eigenschaften ändern."
},
"pdfsToZip": {
"name": "PDFs zu ZIP",
"subtitle": "Mehrere PDF-Dateien in ein ZIP-Archiv packen."
},
"comparePdfs": {
"name": "PDFs vergleichen",
"subtitle": "Zwei PDFs nebeneinander vergleichen."
},
"posterizePdf": {
"name": "PDF posterisieren",
"subtitle": "Eine große Seite in mehrere kleinere Seiten aufteilen."
},
"fixPageSize": {
"name": "Seitengröße reparieren",
"subtitle": "Alle Seiten auf eine einheitliche Größe standardisieren."
},
"linearizePdf": {
"name": "PDF linearisieren",
"subtitle": "PDF für schnelle Web-Anzeige optimieren."
},
"pageDimensions": {
"name": "Seitenmaße",
"subtitle": "Seitengröße, Ausrichtung und Einheiten analysieren."
},
"removeRestrictions": {
"name": "Beschränkungen entfernen",
"subtitle": "Passwortschutz und Sicherheitsbeschränkungen von digital signierten PDF-Dateien entfernen."
},
"repairPdf": {
"name": "PDF reparieren",
"subtitle": "Daten aus beschädigten PDF-Dateien wiederherstellen."
},
"encryptPdf": {
"name": "PDF verschlüsseln",
"subtitle": "Ihre PDF durch Hinzufügen eines Passworts sperren."
},
"sanitizePdf": {
"name": "PDF bereinigen",
"subtitle": "Metadaten, Annotationen, Skripte und mehr entfernen."
},
"decryptPdf": {
"name": "PDF entschlüsseln",
"subtitle": "PDF durch Entfernen des Passwortschutzes entsperren."
},
"flattenPdf": {
"name": "PDF reduzieren",
"subtitle": "Formularfelder und Annotationen nicht editierbar machen."
},
"removeMetadata": {
"name": "Metadaten entfernen",
"subtitle": "Versteckte Daten aus Ihrer PDF entfernen."
},
"changePermissions": {
"name": "Berechtigungen ändern",
"subtitle": "Benutzerberechtigungen für eine PDF festlegen oder ändern."
}
}

View File

@@ -0,0 +1,275 @@
{
"nav": {
"home": "Home",
"about": "About",
"contact": "Contact",
"licensing": "Licensing",
"allTools": "All Tools",
"openMainMenu": "Open main menu",
"language": "Language"
},
"hero": {
"title": "The",
"pdfToolkit": "PDF Toolkit",
"builtForPrivacy": "built for privacy",
"noSignups": "No Signups",
"unlimitedUse": "Unlimited Use",
"worksOffline": "Works Offline",
"startUsing": "Start Using Now"
},
"usedBy": {
"title": "Used by companies and people working at"
},
"features": {
"title": "Why",
"bentoPdf": "BentoPDF?",
"noSignup": {
"title": "No Signup",
"description": "Start instantly, no accounts or emails."
},
"noUploads": {
"title": "No Uploads",
"description": "100% client-side, your files never leave your device."
},
"foreverFree": {
"title": "Forever Free",
"description": "All tools, no trials, no paywalls."
},
"noLimits": {
"title": "No Limits",
"description": "Use as much as you want, no hidden caps."
},
"batchProcessing": {
"title": "Batch Processing",
"description": "Handle unlimited PDFs in one go."
},
"lightningFast": {
"title": "Lightning Fast",
"description": "Process PDFs instantly, without waiting or delays."
}
},
"tools": {
"title": "Get Started with",
"toolsLabel": "Tools",
"subtitle": "Click a tool to open the file uploader",
"searchPlaceholder": "Search for a tool (e.g., 'split', 'organize'...)",
"backToTools": "Back to Tools"
},
"upload": {
"clickToSelect": "Click to select a file",
"orDragAndDrop": "or drag and drop",
"pdfOrImages": "PDFs or Images",
"filesNeverLeave": "Your files never leave your device.",
"addMore": "Add More Files",
"clearAll": "Clear All"
},
"loader": {
"processing": "Processing..."
},
"alert": {
"title": "Alert",
"ok": "OK"
},
"preview": {
"title": "Document Preview",
"downloadAsPdf": "Download as PDF",
"close": "Close"
},
"settings": {
"title": "Settings",
"shortcuts": "Shortcuts",
"preferences": "Preferences",
"displayPreferences": "Display Preferences",
"searchShortcuts": "Search shortcuts...",
"shortcutsInfo": "Press and hold keys to set a shortcut. Changes are auto-saved.",
"shortcutsWarning": "⚠️ Avoid common browser shortcuts (Cmd/Ctrl+W, Cmd/Ctrl+T, Cmd/Ctrl+N etc.) as they may not work reliably.",
"import": "Import",
"export": "Export",
"resetToDefaults": "Reset to Defaults",
"fullWidthMode": "Full Width Mode",
"fullWidthDescription": "Use the full screen width for all tools instead of a centered container",
"settingsAutoSaved": "Settings are automatically saved",
"clickToSet": "Click to set",
"pressKeys": "Press keys...",
"warnings": {
"alreadyInUse": "Shortcut Already in Use",
"assignedTo": "is already assigned to:",
"chooseDifferent": "Please choose a different shortcut.",
"reserved": "Reserved Shortcut Warning",
"commonlyUsed": "is commonly used for:",
"unreliable": "This shortcut may not work reliably or might conflict with browser/system behavior.",
"useAnyway": "Do you want to use it anyway?",
"resetTitle": "Reset Shortcuts",
"resetMessage": "Are you sure you want to reset all shortcuts to default?<br><br>This action cannot be undone.",
"importSuccessTitle": "Import Successful",
"importSuccessMessage": "Shortcuts imported successfully!",
"importFailTitle": "Import Failed",
"importFailMessage": "Failed to import shortcuts. Invalid file format."
}
},
"warning": {
"title": "Warning",
"cancel": "Cancel",
"proceed": "Proceed"
},
"compliance": {
"title": "Your data never leaves your device",
"weKeep": "We keep",
"yourInfoSafe": "your information safe",
"byFollowingStandards": "by following global security standards.",
"processingLocal": "All the processing happens locally on your device.",
"gdpr": {
"title": "GDPR compliance",
"description": "Protects the personal data and privacy of individuals within the European Union."
},
"ccpa": {
"title": "CCPA compliance",
"description": "Gives California residents rights over how their personal information is collected, used, and shared."
},
"hipaa": {
"title": "HIPAA compliance",
"description": "Sets safeguards for handling sensitive health information in the United States healthcare system."
}
},
"faq": {
"title": "Frequently Asked",
"questions": "Questions",
"isFree": {
"question": "Is BentoPDF really free?",
"answer": "Yes, absolutely. All tools on BentoPDF are 100% free to use, with no file limits, no sign-ups, and no watermarks. We believe everyone deserves access to simple, powerful PDF tools without a paywall."
},
"areFilesSecure": {
"question": "Are my files secure? Where are they processed?",
"answer": "Your files are as secure as possible because they never leave your computer. All processing happens directly in your web browser (client-side). We never upload your files to a server, so you maintain complete privacy and control over your documents."
},
"platforms": {
"question": "Does it work on Mac, Windows, and Mobile?",
"answer": "Yes! Since BentoPDF runs entirely in your browser, it works on any operating system with a modern web browser, including Windows, macOS, Linux, iOS, and Android."
},
"gdprCompliant": {
"question": "Is BentoPDF GDPR compliant?",
"answer": "Yes. BentoPDF is fully GDPR compliant. Since all file processing happens locally in your browser and we never collect or transmit your files to any server, we have no access to your data. This ensures you are always in control of your documents."
},
"dataStorage": {
"question": "Do you store or track any of my files?",
"answer": "No. We never store, track, or log your files. Everything you do on BentoPDF happens in your browser memory and disappears once you close the page. There are no uploads, no history logs, and no servers involved."
},
"different": {
"question": "What makes BentoPDF different from other PDF tools?",
"answer": "Most PDF tools upload your files to a server for processing. BentoPDF never does that. We use secure, modern web technology to process your files directly in your browser. This means faster performance, stronger privacy, and complete peace of mind."
},
"browserBased": {
"question": "How does browser-based processing keep me safe?",
"answer": "By running entirely inside your browser, BentoPDF ensures that your files never leave your device. This eliminates the risks of server hacks, data breaches, or unauthorized access. Your files remain yours—always."
},
"analytics": {
"question": "Do you use cookies or analytics to track me?",
"answer": "We care about your privacy. BentoPDF does not track personal information. We use Simple Analytics solely to see anonymous visit counts. This means we can know how many users visit our site, but we never know who you are. Simple Analytics is fully GDPR-compliant and respects your privacy."
}
},
"testimonials": {
"title": "What Our",
"users": "Users",
"say": "Say"
},
"support": {
"title": "Like My Work?",
"description": "BentoPDF is a passion project, built to provide a free, private, and powerful PDF toolkit for everyone. If you find it useful, consider supporting its development. Every coffee helps!",
"buyMeCoffee": "Buy Me a Coffee"
},
"footer": {
"copyright": "© 2025 BentoPDF. All rights reserved.",
"version": "Version",
"company": "Company",
"aboutUs": "About Us",
"faqLink": "FAQ",
"contactUs": "Contact Us",
"legal": "Legal",
"termsAndConditions": "Terms and Conditions",
"privacyPolicy": "Privacy Policy",
"followUs": "Follow Us"
},
"merge": {
"title": "Merge PDFs",
"description": "Combine whole files, or select specific pages to merge into a new document.",
"fileMode": "File Mode",
"pageMode": "Page Mode",
"howItWorks": "How it works:",
"fileModeInstructions": [
"Click and drag the icon to change the order of the files.",
"In the \"Pages\" box for each file, you can specify ranges (e.g., \"1-3, 5\") to merge only those pages.",
"Leave the \"Pages\" box blank to include all pages from that file."
],
"pageModeInstructions": [
"All pages from your uploaded PDFs are shown below.",
"Simply drag and drop the individual page thumbnails to create the exact order you want for your new file."
],
"mergePdfs": "Merge PDFs"
},
"common": {
"page": "Page",
"pages": "Pages",
"of": "of",
"download": "Download",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"add": "Add",
"remove": "Remove",
"loading": "Loading...",
"error": "Error",
"success": "Success",
"file": "File",
"files": "Files"
},
"about": {
"hero": {
"title": "We believe PDF tools should be",
"subtitle": "fast, private, and free.",
"noCompromises": "No compromises."
},
"mission": {
"title": "Our Mission",
"description": "To provide the most comprehensive PDF toolbox that respects your privacy and never asks for payment. We believe essential document tools should be accessible to everyone, everywhere, without barriers."
},
"philosophy": {
"label": "Our Core Philosophy",
"title": "Privacy First. Always.",
"description": "In an era where data is a commodity, we take a different approach. All processing for Bentopdf tools happens locally in your browser. This means your files never touch our servers, we never see your documents, and we don't track what you do. Your documents remain completely and unequivocally private. It's not just a feature; it's our foundation."
},
"whyBentopdf": {
"title": "Why",
"speed": {
"title": "Built for Speed",
"description": "No waiting for uploads or downloads to a server. By processing files directly in your browser using modern web technologies like WebAssembly, we offer unparalleled speed for all our tools."
},
"free": {
"title": "Completely Free",
"description": "No trials, no subscriptions, no hidden fees, and no \"premium\" features held hostage. We believe powerful PDF tools should be a public utility, not a profit center."
},
"noAccount": {
"title": "No Account Required",
"description": "Start using any tool immediately. We don't need your email, a password, or any personal information. Your workflow should be frictionless and anonymous."
},
"openSource": {
"title": "Open Source Spirit",
"description": "Built with transparency in mind. We leverage incredible open-source libraries like PDF-lib and PDF.js, and believe in the community-driven effort to make powerful tools accessible to everyone."
}
},
"cta": {
"title": "Ready to get started?",
"description": "Join thousands of users who trust Bentopdf for their daily document needs. Experience the difference that privacy and performance can make.",
"button": "Explore All Tools"
}
},
"contact": {
"title": "Get in Touch",
"subtitle": "We'd love to hear from you. Whether you have a question, feedback, or a feature request, please don't hesitate to reach out.",
"email": "You can reach us directly by email at:"
},
"licensing": {
"title": "Licensing for",
"subtitle": "Choose the license that fits your needs."
}
}

View File

@@ -0,0 +1,279 @@
{
"categories": {
"popularTools": "Popular Tools",
"editAnnotate": "Edit & Annotate",
"convertToPdf": "Convert to PDF",
"convertFromPdf": "Convert from PDF",
"organizeManage": "Organize & Manage",
"optimizeRepair": "Optimize & Repair",
"securePdf": "Secure PDF"
},
"pdfMultiTool": {
"name": "PDF Multi Tool",
"subtitle": "Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface."
},
"mergePdf": {
"name": "Merge PDF",
"subtitle": "Combine multiple PDFs into one file. Preserves Bookmarks."
},
"splitPdf": {
"name": "Split PDF",
"subtitle": "Extract a range of pages into a new PDF."
},
"compressPdf": {
"name": "Compress PDF",
"subtitle": "Reduce the file size of your PDF."
},
"pdfEditor": {
"name": "PDF Editor",
"subtitle": "Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs."
},
"jpgToPdf": {
"name": "JPG to PDF",
"subtitle": "Create a PDF from one or more JPG images."
},
"signPdf": {
"name": "Sign PDF",
"subtitle": "Draw, type, or upload your signature."
},
"cropPdf": {
"name": "Crop PDF",
"subtitle": "Trim the margins of every page in your PDF."
},
"extractPages": {
"name": "Extract Pages",
"subtitle": "Save a selection of pages as new files."
},
"duplicateOrganize": {
"name": "Duplicate & Organize",
"subtitle": "Duplicate, reorder, and delete pages."
},
"deletePages": {
"name": "Delete Pages",
"subtitle": "Remove specific pages from your document."
},
"editBookmarks": {
"name": "Edit Bookmarks",
"subtitle": "Add, edit, import, delete and extract PDF bookmarks."
},
"tableOfContents": {
"name": "Table of Contents",
"subtitle": "Generate a table of contents page from PDF bookmarks."
},
"pageNumbers": {
"name": "Page Numbers",
"subtitle": "Insert page numbers into your document."
},
"addWatermark": {
"name": "Add Watermark",
"subtitle": "Stamp text or an image over your PDF pages."
},
"headerFooter": {
"name": "Header & Footer",
"subtitle": "Add text to the top and bottom of pages."
},
"invertColors": {
"name": "Invert Colors",
"subtitle": "Create a \"dark mode\" version of your PDF."
},
"backgroundColor": {
"name": "Background Color",
"subtitle": "Change the background color of your PDF."
},
"changeTextColor": {
"name": "Change Text Color",
"subtitle": "Change the color of text in your PDF."
},
"addStamps": {
"name": "Add Stamps",
"subtitle": "Add image stamps to your PDF using the annotation toolbar."
},
"removeAnnotations": {
"name": "Remove Annotations",
"subtitle": "Strip comments, highlights, and links."
},
"pdfFormFiller": {
"name": "PDF Form Filler",
"subtitle": "Fill in forms directly in the browser. Also supports XFA forms."
},
"createPdfForm": {
"name": "Create PDF Form",
"subtitle": "Create fillable PDF forms with drag-and-drop text fields."
},
"removeBlankPages": {
"name": "Remove Blank Pages",
"subtitle": "Automatically detect and delete blank pages."
},
"imageToPdf": {
"name": "Image to PDF",
"subtitle": "Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF."
},
"pngToPdf": {
"name": "PNG to PDF",
"subtitle": "Create a PDF from one or more PNG images."
},
"webpToPdf": {
"name": "WebP to PDF",
"subtitle": "Create a PDF from one or more WebP images."
},
"svgToPdf": {
"name": "SVG to PDF",
"subtitle": "Create a PDF from one or more SVG images."
},
"bmpToPdf": {
"name": "BMP to PDF",
"subtitle": "Create a PDF from one or more BMP images."
},
"heicToPdf": {
"name": "HEIC to PDF",
"subtitle": "Create a PDF from one or more HEIC images."
},
"tiffToPdf": {
"name": "TIFF to PDF",
"subtitle": "Create a PDF from one or more TIFF images."
},
"textToPdf": {
"name": "Text to PDF",
"subtitle": "Convert a plain text file into a PDF."
},
"jsonToPdf": {
"name": "JSON to PDF",
"subtitle": "Convert JSON files to PDF format."
},
"pdfToJpg": {
"name": "PDF to JPG",
"subtitle": "Convert each PDF page into a JPG image."
},
"pdfToPng": {
"name": "PDF to PNG",
"subtitle": "Convert each PDF page into a PNG image."
},
"pdfToWebp": {
"name": "PDF to WebP",
"subtitle": "Convert each PDF page into a WebP image."
},
"pdfToBmp": {
"name": "PDF to BMP",
"subtitle": "Convert each PDF page into a BMP image."
},
"pdfToTiff": {
"name": "PDF to TIFF",
"subtitle": "Convert each PDF page into a TIFF image."
},
"pdfToGreyscale": {
"name": "PDF to Greyscale",
"subtitle": "Convert all colors to black and white."
},
"pdfToJson": {
"name": "PDF to JSON",
"subtitle": "Convert PDF files to JSON format."
},
"ocrPdf": {
"name": "OCR PDF",
"subtitle": "Make a PDF searchable and copyable."
},
"alternateMix": {
"name": "Alternate & Mix Pages",
"subtitle": "Merge PDFs by alternating pages from each PDF. Preserves Bookmarks."
},
"addAttachments": {
"name": "Add Attachments",
"subtitle": "Embed one or more files into your PDF."
},
"extractAttachments": {
"name": "Extract Attachments",
"subtitle": "Extract all embedded files from PDF(s) as a ZIP."
},
"editAttachments": {
"name": "Edit Attachments",
"subtitle": "View or remove attachments in your PDF."
},
"dividePages": {
"name": "Divide Pages",
"subtitle": "Divide pages horizontally or vertically."
},
"addBlankPage": {
"name": "Add Blank Page",
"subtitle": "Insert an empty page anywhere in your PDF."
},
"reversePages": {
"name": "Reverse Pages",
"subtitle": "Flip the order of all pages in your document."
},
"rotatePdf": {
"name": "Rotate PDF",
"subtitle": "Turn pages in 90-degree increments."
},
"nUpPdf": {
"name": "N-Up PDF",
"subtitle": "Arrange multiple pages onto a single sheet."
},
"combineToSinglePage": {
"name": "Combine to Single Page",
"subtitle": "Stitch all pages into one continuous scroll."
},
"viewMetadata": {
"name": "View Metadata",
"subtitle": "Inspect the hidden properties of your PDF."
},
"editMetadata": {
"name": "Edit Metadata",
"subtitle": "Change the author, title, and other properties."
},
"pdfsToZip": {
"name": "PDFs to ZIP",
"subtitle": "Package multiple PDF files into a ZIP archive."
},
"comparePdfs": {
"name": "Compare PDFs",
"subtitle": "Compare two PDFs side by side."
},
"posterizePdf": {
"name": "Posterize PDF",
"subtitle": "Split a large page into multiple smaller pages."
},
"fixPageSize": {
"name": "Fix Page Size",
"subtitle": "Standardize all pages to a uniform size."
},
"linearizePdf": {
"name": "Linearize PDF",
"subtitle": "Optimize PDF for fast web viewing."
},
"pageDimensions": {
"name": "Page Dimensions",
"subtitle": "Analyze page size, orientation, and units."
},
"removeRestrictions": {
"name": "Remove Restrictions",
"subtitle": "Remove password protection and security restrictions associated with digitally signed PDF files."
},
"repairPdf": {
"name": "Repair PDF",
"subtitle": "Recover data from corrupted or damaged PDF files."
},
"encryptPdf": {
"name": "Encrypt PDF",
"subtitle": "Lock your PDF by adding a password."
},
"sanitizePdf": {
"name": "Sanitize PDF",
"subtitle": "Remove metadata, annotations, scripts, and more."
},
"decryptPdf": {
"name": "Decrypt PDF",
"subtitle": "Unlock PDF by removing password protection."
},
"flattenPdf": {
"name": "Flatten PDF",
"subtitle": "Make form fields and annotations non-editable."
},
"removeMetadata": {
"name": "Remove Metadata",
"subtitle": "Strip hidden data from your PDF."
},
"changePermissions": {
"name": "Change Permissions",
"subtitle": "Set or change user permissions on a PDF."
}
}

236
scripts/check-translations.js Executable file
View File

@@ -0,0 +1,236 @@
#!/usr/bin/env node
/**
* Translation Checker for BentoPDF
*
* This script compares translation files across languages and reports:
* - Missing keys (keys present in English but absent in other languages)
* - Extra keys (keys present in other languages but not in English)
* - Untranslated keys (keys with the same value as English)
*
* Usage:
* node scripts/check-translations.js
* node scripts/check-translations.js --verbose
* node scripts/check-translations.js --lang=de
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const LOCALES_DIR = path.join(__dirname, '../public/locales');
const REFERENCE_LANG = 'en';
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
dim: '\x1b[2m',
};
const args = process.argv.slice(2);
const verbose = args.includes('--verbose');
const specificLang = args.find(arg => arg.startsWith('--lang='))?.split('=')[1];
/**
* Flatten nested JSON object into dot notation
* { a: { b: 'value' } } => { 'a.b': 'value' }
*/
function flattenObject(obj, prefix = '') {
const flattened = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(flattened, flattenObject(value, newKey));
} else {
flattened[newKey] = value;
}
}
return flattened;
}
/**
* Load and parse a translation file
*/
function loadTranslation(lang) {
const filePath = path.join(LOCALES_DIR, lang, 'common.json');
if (!fs.existsSync(filePath)) {
return null;
}
try {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
console.error(`${colors.red}✗ Error parsing ${lang}/common.json:${colors.reset}`, error.message);
return null;
}
}
/**
* Get all available languages
*/
function getAvailableLanguages() {
if (!fs.existsSync(LOCALES_DIR)) {
console.error(`${colors.red}✗ Locales directory not found: ${LOCALES_DIR}${colors.reset}`);
process.exit(1);
}
return fs.readdirSync(LOCALES_DIR)
.filter(item => {
const itemPath = path.join(LOCALES_DIR, item);
return fs.statSync(itemPath).isDirectory();
})
.filter(lang => {
// Only include if common.json exists
return fs.existsSync(path.join(LOCALES_DIR, lang, 'common.json'));
});
}
/**
* Compare two sets of keys and report differences
*/
function compareKeys(refKeys, targetKeys, refLang, targetLang, refFlat, targetFlat) {
const missing = refKeys.filter(key => !targetKeys.includes(key));
const extra = targetKeys.filter(key => !refKeys.includes(key));
const untranslated = refKeys.filter(key =>
targetKeys.includes(key) && refFlat[key] === targetFlat[key] && typeof refFlat[key] === 'string'
);
return { missing, extra, untranslated };
}
/**
* Print section header
*/
function printHeader(text) {
console.log(`\n${colors.cyan}${'='.repeat(60)}${colors.reset}`);
console.log(`${colors.cyan}${text}${colors.reset}`);
console.log(`${colors.cyan}${'='.repeat(60)}${colors.reset}\n`);
}
/**
* Print section
*/
function printSection(title, items, color = colors.yellow) {
if (items.length === 0) return;
console.log(`${color}${title} (${items.length}):${colors.reset}`);
items.forEach(item => {
console.log(` ${colors.dim}${colors.reset} ${item}`);
});
console.log();
}
/**
* Main function
*/
function main() {
console.log(`${colors.blue}🌍 BentoPDF Translation Checker${colors.reset}\n`);
const languages = getAvailableLanguages();
if (languages.length === 0) {
console.error(`${colors.red}✗ No translation files found in ${LOCALES_DIR}${colors.reset}`);
process.exit(1);
}
// Load reference language (English)
const refTranslation = loadTranslation(REFERENCE_LANG);
if (!refTranslation) {
console.error(`${colors.red}✗ Reference language (${REFERENCE_LANG}) not found${colors.reset}`);
process.exit(1);
}
const refFlat = flattenObject(refTranslation);
const refKeys = Object.keys(refFlat);
console.log(`${colors.green}✓ Reference language (${REFERENCE_LANG}): ${refKeys.length} keys${colors.reset}`);
console.log(`${colors.dim} Available languages: ${languages.join(', ')}${colors.reset}\n`);
// Filter languages to check
const langsToCheck = specificLang
? languages.filter(lang => lang === specificLang)
: languages.filter(lang => lang !== REFERENCE_LANG);
if (langsToCheck.length === 0) {
console.log(`${colors.yellow}⚠ No languages to check${colors.reset}`);
process.exit(0);
}
let hasIssues = false;
// Check each language
for (const lang of langsToCheck) {
printHeader(`Checking: ${lang.toUpperCase()}`);
const translation = loadTranslation(lang);
if (!translation) {
hasIssues = true;
continue;
}
const targetFlat = flattenObject(translation);
const targetKeys = Object.keys(targetFlat);
const { missing, extra, untranslated } = compareKeys(
refKeys,
targetKeys,
REFERENCE_LANG,
lang,
refFlat,
targetFlat
);
// Summary
console.log(`${colors.dim}Total keys: ${targetKeys.length} / ${refKeys.length}${colors.reset}\n`);
// Missing keys
if (missing.length > 0) {
hasIssues = true;
printSection(`Missing Keys`, missing, colors.red);
}
// Extra keys
if (extra.length > 0) {
hasIssues = true;
printSection(`Extra Keys (not in English)`, extra, colors.yellow);
}
// Untranslated keys (same as English)
if (verbose && untranslated.length > 0) {
printSection(`Possibly Untranslated (same as English)`, untranslated, colors.cyan);
} else if (untranslated.length > 0) {
console.log(`${colors.cyan}Possibly Untranslated: ${untranslated.length}${colors.reset}`);
console.log(`${colors.dim} (use --verbose to see details)${colors.reset}\n`);
}
// All good
if (missing.length === 0 && extra.length === 0) {
console.log(`${colors.green} No missing or extra keys!${colors.reset}\n`);
}
}
// Final summary
console.log(`${colors.cyan}${'='.repeat(60)}${colors.reset}\n`);
if (!hasIssues) {
console.log(`${colors.green} All translations are in sync!${colors.reset}\n`);
process.exit(0);
} else {
console.log(`${colors.yellow} Issues found. Please review the output above.${colors.reset}\n`);
process.exit(1);
}
}
main();

View File

@@ -4,68 +4,68 @@ export const categories = [
name: 'Popular Tools',
tools: [
{
href: import.meta.env.BASE_URL + 'src/pages/pdf-multi-tool.html',
href: import.meta.env.BASE_URL + 'pdf-multi-tool.html',
name: 'PDF Multi Tool',
icon: 'pencil-ruler',
subtitle: 'Merge, Split, Organize, Delete, Rotate, Add Blank Pages, Extract and Duplicate in an unified interface.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/merge-pdf.html',
href: import.meta.env.BASE_URL + 'merge-pdf.html',
name: 'Merge PDF',
icon: 'combine',
subtitle: 'Combine multiple PDFs into one file. Preserves Bookmarks.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/split-pdf.html',
href: import.meta.env.BASE_URL + 'split-pdf.html',
name: 'Split PDF',
icon: 'scissors',
subtitle: 'Extract a range of pages into a new PDF.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/compress-pdf.html',
href: import.meta.env.BASE_URL + 'compress-pdf.html',
name: 'Compress PDF',
icon: 'zap',
subtitle: 'Reduce the file size of your PDF.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/edit-pdf.html',
href: import.meta.env.BASE_URL + 'edit-pdf.html',
name: 'PDF Editor',
icon: 'pocket-knife',
subtitle:
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs',
},
{
href: import.meta.env.BASE_URL + 'src/pages/jpg-to-pdf.html',
href: import.meta.env.BASE_URL + 'jpg-to-pdf.html',
name: 'JPG to PDF',
icon: 'image-up',
subtitle: 'Create a PDF from one or more JPG images.',
},
{
id: 'sign-pdf',
href: import.meta.env.BASE_URL + 'sign-pdf.html',
name: 'Sign PDF',
icon: 'pen-tool',
subtitle: 'Draw, type, or upload your signature.',
},
{
id: 'cropper',
href: import.meta.env.BASE_URL + 'crop-pdf.html',
name: 'Crop PDF',
icon: 'crop',
subtitle: 'Trim the margins of every page in your PDF.',
},
{
id: 'extract-pages',
href: import.meta.env.BASE_URL + 'extract-pages.html',
name: 'Extract Pages',
icon: 'ungroup',
subtitle: 'Save a selection of pages as new files.',
},
{
id: 'duplicate-organize',
href: import.meta.env.BASE_URL + 'organize-pdf.html',
name: 'Duplicate & Organize',
icon: 'files',
subtitle: 'Duplicate, reorder, and delete pages.',
},
{
id: 'delete-pages',
href: import.meta.env.BASE_URL + 'delete-pages.html',
name: 'Delete Pages',
icon: 'trash-2',
subtitle: 'Remove specific pages from your document.',
@@ -76,99 +76,98 @@ export const categories = [
name: 'Edit & Annotate',
tools: [
{
href: import.meta.env.BASE_URL + 'src/pages/edit-pdf.html',
href: import.meta.env.BASE_URL + 'edit-pdf.html',
name: 'PDF Editor',
icon: 'pocket-knife',
subtitle:
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.',
},
{
// id: 'bookmark-pdf',
href: import.meta.env.BASE_URL + 'src/pages/bookmark.html',
href: import.meta.env.BASE_URL + 'bookmark.html',
name: 'Edit Bookmarks',
icon: 'bookmark',
subtitle: 'Add, edit, import, delete and extract PDF bookmarks.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/table-of-contents.html',
href: import.meta.env.BASE_URL + 'table-of-contents.html',
name: 'Table of Contents',
icon: 'list',
subtitle: 'Generate a table of contents page from PDF bookmarks.',
},
{
id: 'add-page-numbers',
href: import.meta.env.BASE_URL + 'page-numbers.html',
name: 'Page Numbers',
icon: 'list-ordered',
subtitle: 'Insert page numbers into your document.',
},
{
id: 'add-watermark',
href: import.meta.env.BASE_URL + 'add-watermark.html',
name: 'Add Watermark',
icon: 'droplets',
subtitle: 'Stamp text or an image over your PDF pages.',
},
{
id: 'add-header-footer',
href: import.meta.env.BASE_URL + 'header-footer.html',
name: 'Header & Footer',
icon: 'pilcrow',
subtitle: 'Add text to the top and bottom of pages.',
},
{
id: 'invert-colors',
href: import.meta.env.BASE_URL + 'invert-colors.html',
name: 'Invert Colors',
icon: 'contrast',
subtitle: 'Create a "dark mode" version of your PDF.',
},
{
id: 'change-background-color',
href: import.meta.env.BASE_URL + 'background-color.html',
name: 'Background Color',
icon: 'palette',
subtitle: 'Change the background color of your PDF.',
},
{
id: 'change-text-color',
href: import.meta.env.BASE_URL + 'text-color.html',
name: 'Change Text Color',
icon: 'type',
subtitle: 'Change the color of text in your PDF.',
},
{
id: 'sign-pdf',
href: import.meta.env.BASE_URL + 'sign-pdf.html',
name: 'Sign PDF',
icon: 'pen-tool',
subtitle: 'Draw, type, or upload your signature.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/add-stamps.html',
href: import.meta.env.BASE_URL + 'add-stamps.html',
name: 'Add Stamps',
icon: 'stamp',
subtitle: 'Add image stamps to your PDF using the annotation toolbar.',
},
{
id: 'remove-annotations',
href: import.meta.env.BASE_URL + 'remove-annotations.html',
name: 'Remove Annotations',
icon: 'eraser',
subtitle: 'Strip comments, highlights, and links.',
},
{
id: 'cropper',
href: import.meta.env.BASE_URL + 'crop-pdf.html',
name: 'Crop PDF',
icon: 'crop',
subtitle: 'Trim the margins of every page in your PDF.',
},
{
id: 'form-filler',
href: import.meta.env.BASE_URL + 'form-filler.html',
name: 'PDF Form Filler',
icon: 'square-pen',
subtitle: 'Fill in forms directly in the browser. Also supports XFA forms.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/form-creator.html',
href: import.meta.env.BASE_URL + 'form-creator.html',
name: 'Create PDF Form',
icon: 'file-input',
subtitle: 'Create fillable PDF forms with drag-and-drop text fields.',
},
{
id: 'remove-blank-pages',
href: import.meta.env.BASE_URL + 'remove-blank-pages.html',
name: 'Remove Blank Pages',
icon: 'file-minus-2',
subtitle: 'Automatically detect and delete blank pages.',
@@ -179,255 +178,245 @@ export const categories = [
name: 'Convert to PDF',
tools: [
{
id: 'image-to-pdf',
href: import.meta.env.BASE_URL + 'image-to-pdf.html',
name: 'Image to PDF',
icon: 'images',
subtitle: 'Convert JPG, PNG, WebP, BMP, TIFF, SVG, HEIC to PDF.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/jpg-to-pdf.html',
href: import.meta.env.BASE_URL + 'jpg-to-pdf.html',
name: 'JPG to PDF',
icon: 'image-up',
subtitle: 'Create a PDF from one or more JPG images.',
},
{
id: 'png-to-pdf',
href: import.meta.env.BASE_URL + 'png-to-pdf.html',
name: 'PNG to PDF',
icon: 'image-up',
subtitle: 'Create a PDF from one or more PNG images.',
},
{
id: 'webp-to-pdf',
href: import.meta.env.BASE_URL + 'webp-to-pdf.html',
name: 'WebP to PDF',
icon: 'image-up',
subtitle: 'Create a PDF from one or more WebP images.',
},
{
id: 'svg-to-pdf',
href: import.meta.env.BASE_URL + 'svg-to-pdf.html',
name: 'SVG to PDF',
icon: 'pen-tool',
subtitle: 'Create a PDF from one or more SVG images.',
},
{
id: 'bmp-to-pdf',
href: import.meta.env.BASE_URL + 'bmp-to-pdf.html',
name: 'BMP to PDF',
icon: 'image',
subtitle: 'Create a PDF from one or more BMP images.',
},
{
id: 'heic-to-pdf',
href: import.meta.env.BASE_URL + 'heic-to-pdf.html',
name: 'HEIC to PDF',
icon: 'smartphone',
subtitle: 'Create a PDF from one or more HEIC images.',
},
{
id: 'tiff-to-pdf',
href: import.meta.env.BASE_URL + 'tiff-to-pdf.html',
name: 'TIFF to PDF',
icon: 'layers',
subtitle: 'Create a PDF from one or more TIFF images.',
},
{
id: 'txt-to-pdf',
href: import.meta.env.BASE_URL + 'txt-to-pdf.html',
name: 'Text to PDF',
icon: 'file-pen',
subtitle: 'Convert a plain text file into a PDF.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/json-to-pdf.html',
href: import.meta.env.BASE_URL + 'json-to-pdf.html',
name: 'JSON to PDF',
icon: 'file-code',
subtitle: 'Convert JSON files to PDF format.',
},
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
],
},
{
name: 'Convert from PDF',
tools: [
{
id: 'pdf-to-jpg',
href: import.meta.env.BASE_URL + 'pdf-to-jpg.html',
name: 'PDF to JPG',
icon: 'file-image',
subtitle: 'Convert each PDF page into a JPG image.',
},
{
id: 'pdf-to-png',
href: import.meta.env.BASE_URL + 'pdf-to-png.html',
name: 'PDF to PNG',
icon: 'file-image',
subtitle: 'Convert each PDF page into a PNG image.',
},
{
id: 'pdf-to-webp',
href: import.meta.env.BASE_URL + 'pdf-to-webp.html',
name: 'PDF to WebP',
icon: 'file-image',
subtitle: 'Convert each PDF page into a WebP image.',
},
{
id: 'pdf-to-bmp',
href: import.meta.env.BASE_URL + 'pdf-to-bmp.html',
name: 'PDF to BMP',
icon: 'file-image',
subtitle: 'Convert each PDF page into a BMP image.',
},
{
id: 'pdf-to-tiff',
href: import.meta.env.BASE_URL + 'pdf-to-tiff.html',
name: 'PDF to TIFF',
icon: 'file-image',
subtitle: 'Convert each PDF page into a TIFF image.',
},
{
id: 'pdf-to-greyscale',
href: import.meta.env.BASE_URL + 'pdf-to-greyscale.html',
name: 'PDF to Greyscale',
icon: 'palette',
subtitle: 'Convert all colors to black and white.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/pdf-to-json.html',
href: import.meta.env.BASE_URL + 'pdf-to-json.html',
name: 'PDF to JSON',
icon: 'file-code',
subtitle: 'Convert PDF files to JSON format.',
},
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
],
},
{
name: 'Organize & Manage',
tools: [
{
id: 'ocr-pdf',
href: import.meta.env.BASE_URL + 'ocr-pdf.html',
name: 'OCR PDF',
icon: 'scan-text',
subtitle: 'Make a PDF searchable and copyable.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/merge-pdf.html',
href: import.meta.env.BASE_URL + 'merge-pdf.html',
name: 'Merge PDF',
icon: 'combine',
subtitle: 'Combine multiple PDFs into one file.',
},
{
id: 'alternate-merge',
href: import.meta.env.BASE_URL + 'alternate-merge.html',
name: 'Alternate & Mix Pages',
icon: 'shuffle',
subtitle: 'Merge PDFs by alternating pages from each PDF. Preserves Bookmarks',
},
{
id: 'organize',
name: 'Organize PDF',
icon: 'grip',
subtitle: 'Reorder pages by dragging and dropping.',
},
{
id: 'duplicate-organize',
name: 'Duplicate & Organize',
href: import.meta.env.BASE_URL + 'organize-pdf.html',
name: 'Organize & Duplicate',
icon: 'files',
subtitle: 'Duplicate, reorder, and delete pages.',
},
{
id: 'add-attachments',
href: import.meta.env.BASE_URL + 'add-attachments.html',
name: 'Add Attachments',
icon: 'paperclip',
subtitle: 'Embed one or more files into your PDF.',
},
{
id: 'extract-attachments',
href: import.meta.env.BASE_URL + 'extract-attachments.html',
name: 'Extract Attachments',
icon: 'download',
subtitle: 'Extract all embedded files from PDF(s) as a ZIP.',
},
{
id: 'edit-attachments',
href: import.meta.env.BASE_URL + 'edit-attachments.html',
name: 'Edit Attachments',
icon: 'file-edit',
subtitle: 'View or remove attachments in your PDF.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/pdf-multi-tool.html',
href: import.meta.env.BASE_URL + 'pdf-multi-tool.html',
name: 'PDF Multi Tool',
icon: 'pencil-ruler',
subtitle: 'Full-featured PDF editor with page management.',
},
{
id: 'split',
href: import.meta.env.BASE_URL + 'split-pdf.html',
name: 'Split PDF',
icon: 'scissors',
subtitle: 'Extract a range of pages into a new PDF.',
},
{
id: 'split-in-half',
href: import.meta.env.BASE_URL + 'divide-pages.html',
name: 'Divide Pages',
icon: 'table-columns-split',
subtitle: 'Divide pages horizontally or vertically.',
},
{
id: 'extract-pages',
href: import.meta.env.BASE_URL + 'extract-pages.html',
name: 'Extract Pages',
icon: 'ungroup',
subtitle: 'Save a selection of pages as new files.',
},
{
id: 'delete-pages',
href: import.meta.env.BASE_URL + 'delete-pages.html',
name: 'Delete Pages',
icon: 'trash-2',
subtitle: 'Remove specific pages from your document.',
},
{
id: 'add-blank-page',
href: import.meta.env.BASE_URL + 'add-blank-page.html',
name: 'Add Blank Page',
icon: 'file-plus-2',
subtitle: 'Insert an empty page anywhere in your PDF.',
},
{
id: 'reverse-pages',
href: import.meta.env.BASE_URL + 'reverse-pages.html',
name: 'Reverse Pages',
icon: 'arrow-down-z-a',
subtitle: 'Flip the order of all pages in your document.',
},
{
id: 'rotate',
href: import.meta.env.BASE_URL + 'rotate-pdf.html',
name: 'Rotate PDF',
icon: 'rotate-cw',
subtitle: 'Turn pages in 90-degree increments.',
},
{
id: 'n-up',
href: import.meta.env.BASE_URL + 'n-up-pdf.html',
name: 'N-Up PDF',
icon: 'layout-grid',
subtitle: 'Arrange multiple pages onto a single sheet.',
},
{
id: 'combine-single-page',
href: import.meta.env.BASE_URL + 'combine-single-page.html',
name: 'Combine to Single Page',
icon: 'unfold-vertical',
subtitle: 'Stitch all pages into one continuous scroll.',
},
{
id: 'view-metadata',
href: import.meta.env.BASE_URL + 'view-metadata.html',
name: 'View Metadata',
icon: 'info',
subtitle: 'Inspect the hidden properties of your PDF.',
},
{
id: 'edit-metadata',
href: import.meta.env.BASE_URL + 'edit-metadata.html',
name: 'Edit Metadata',
icon: 'file-cog',
subtitle: 'Change the author, title, and other properties.',
},
{
id: 'pdf-to-zip',
href: import.meta.env.BASE_URL + 'pdf-to-zip.html',
name: 'PDFs to ZIP',
icon: 'stretch-horizontal',
subtitle: 'Package multiple PDF files into a ZIP archive.',
},
{
id: 'compare-pdfs',
href: import.meta.env.BASE_URL + 'compare-pdfs.html',
name: 'Compare PDFs',
icon: 'git-compare',
subtitle: 'Compare two PDFs side by side.',
},
{
id: 'posterize',
href: import.meta.env.BASE_URL + 'posterize-pdf.html',
name: 'Posterize PDF',
icon: 'notepad-text-dashed',
subtitle: 'Split a large page into multiple smaller pages.',
@@ -438,38 +427,38 @@ export const categories = [
name: 'Optimize & Repair',
tools: [
{
href: import.meta.env.BASE_URL + 'src/pages/compress-pdf.html',
href: import.meta.env.BASE_URL + 'compress-pdf.html',
name: 'Compress PDF',
icon: 'zap',
subtitle: 'Reduce the file size of your PDF.',
},
{
id: 'fix-dimensions',
href: import.meta.env.BASE_URL + 'fix-page-size.html',
name: 'Fix Page Size',
icon: 'ruler-dimension-line',
subtitle: 'Standardize all pages to a uniform size.',
},
{
id: 'linearize',
href: import.meta.env.BASE_URL + 'linearize-pdf.html',
name: 'Linearize PDF',
icon: 'gauge',
subtitle: 'Optimize PDF for fast web viewing.',
},
{
id: 'page-dimensions',
href: import.meta.env.BASE_URL + 'page-dimensions.html',
name: 'Page Dimensions',
icon: 'ruler',
subtitle: 'Analyze page size, orientation, and units.',
},
{
id: 'remove-restrictions',
href: import.meta.env.BASE_URL + 'remove-restrictions.html',
name: 'Remove Restrictions',
icon: 'unlink',
subtitle:
'Remove password protection and security restrictions associated with digitally signed PDF files.',
},
{
href: import.meta.env.BASE_URL + 'src/pages/repair-pdf.html',
href: import.meta.env.BASE_URL + 'repair-pdf.html',
name: 'Repair PDF',
icon: 'wrench',
subtitle: 'Recover data from corrupted or damaged PDF files.',
@@ -480,37 +469,37 @@ export const categories = [
name: 'Secure PDF',
tools: [
{
id: 'encrypt',
href: import.meta.env.BASE_URL + 'encrypt-pdf.html',
name: 'Encrypt PDF',
icon: 'lock',
subtitle: 'Lock your PDF by adding a password.',
},
{
id: 'sanitize-pdf',
href: import.meta.env.BASE_URL + 'sanitize-pdf.html',
name: 'Sanitize PDF',
icon: 'brush-cleaning',
subtitle: 'Remove metadata, annotations, scripts, and more.',
},
{
id: 'decrypt',
href: import.meta.env.BASE_URL + 'decrypt-pdf.html',
name: 'Decrypt PDF',
icon: 'unlock',
subtitle: 'Unlock PDF by removing password protection.',
},
{
id: 'flatten',
href: import.meta.env.BASE_URL + 'flatten-pdf.html',
name: 'Flatten PDF',
icon: 'layers',
subtitle: 'Make form fields and annotations non-editable.',
},
{
id: 'remove-metadata',
href: import.meta.env.BASE_URL + 'remove-metadata.html',
name: 'Remove Metadata',
icon: 'file-x',
subtitle: 'Strip hidden data from your PDF.',
},
{
id: 'change-permissions',
href: import.meta.env.BASE_URL + 'change-permissions.html',
name: 'Change Permissions',
icon: 'shield-check',
subtitle: 'Set or change user permissions on a PDF.',

View File

@@ -1,5 +1,5 @@
// Simple FAQ accordion handler for standalone pages
document.addEventListener('DOMContentLoaded', () => {
window.addEventListener('load', () => {
const faqAccordion = document.getElementById('faq-accordion');
if (faqAccordion) {
faqAccordion.addEventListener('click', (e) => {

View File

@@ -23,23 +23,13 @@ import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
// Re-export rotation state utilities
export { getRotationState, updateRotationState, resetRotationState, initializeRotationState } from '../utils/rotation-state.js';
const rotationState: number[] = [];
let imageSortableInstance: Sortable | null = null;
const activeImageUrls = new Map<File, string>();
export function getRotationState(): readonly number[] {
return rotationState;
}
export function updateRotationState(pageIndex: number, rotation: number) {
if (pageIndex >= 0 && pageIndex < rotationState.length) {
rotationState[pageIndex] = rotation;
}
}
export function resetRotationState() {
rotationState.length = 0;
}
async function handleSinglePdfUpload(toolId, file) {
showLoader('Loading PDF...');

View File

@@ -1,41 +0,0 @@
import { state } from '../state.js';
import { dom, switchView, toolTemplates } from '../ui.js';
import { setupFileInputHandler } from './fileHandler.js';
import { toolLogic } from '../logic/index.js';
import { createIcons, icons } from 'lucide';
const SETUP_AFTER_UPLOAD = ['sign-pdf'];
export function setupToolInterface(toolId: any) {
window.scrollTo({
top: 0,
left: 0,
behavior: 'instant' as ScrollBehavior,
});
state.activeTool = toolId;
dom.toolContent.innerHTML = toolTemplates[toolId]();
createIcons({ icons });
switchView('tool');
const fileInput = document.getElementById('file-input');
const processBtn = document.getElementById('process-btn');
if (!fileInput && processBtn) {
const logic = toolLogic[toolId];
if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic;
processBtn.onclick = func;
}
}
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
if (!SETUP_AFTER_UPLOAD.includes(toolId)) {
toolLogic[toolId].setup();
}
}
if (fileInput) {
setupFileInputHandler(toolId);
}
}

154
src/js/i18n/i18n.ts Normal file
View File

@@ -0,0 +1,154 @@
import i18next from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpBackend from 'i18next-http-backend';
// Supported languages
export const supportedLanguages = ['en', 'de'] as const;
export type SupportedLanguage = (typeof supportedLanguages)[number];
export const languageNames: Record<SupportedLanguage, string> = {
en: 'English',
de: 'Deutsch',
};
export const getLanguageFromUrl = (): SupportedLanguage => {
const path = window.location.pathname;
const langMatch = path.match(/^\/(en|de)(?:\/|$)/);
if (langMatch && supportedLanguages.includes(langMatch[1] as SupportedLanguage)) {
return langMatch[1] as SupportedLanguage;
}
const storedLang = localStorage.getItem('i18nextLng');
if (storedLang && supportedLanguages.includes(storedLang as SupportedLanguage)) {
return storedLang as SupportedLanguage;
}
return 'en';
};
let initialized = false;
export const initI18n = async (): Promise<typeof i18next> => {
if (initialized) return i18next;
const currentLang = getLanguageFromUrl();
await i18next
.use(HttpBackend)
.use(LanguageDetector)
.init({
lng: currentLang,
fallbackLng: 'en',
supportedLngs: supportedLanguages as unknown as string[],
ns: ['common', 'tools'],
defaultNS: 'common',
backend: {
loadPath: `${import.meta.env.BASE_URL}locales/{{lng}}/{{ns}}.json`,
},
detection: {
order: ['path', 'localStorage', 'navigator'],
lookupFromPathIndex: 0,
caches: ['localStorage'],
},
interpolation: {
escapeValue: false,
},
});
initialized = true;
return i18next;
};
export const t = (key: string, options?: Record<string, unknown>): string => {
return i18next.t(key, options);
};
export const changeLanguage = (lang: SupportedLanguage): void => {
if (!supportedLanguages.includes(lang)) return;
const currentPath = window.location.pathname;
const currentLang = getLanguageFromUrl();
let newPath: string;
if (currentPath.match(/^\/(en|de)\//)) {
newPath = currentPath.replace(/^\/(en|de)\//, `/${lang}/`);
} else if (currentPath.match(/^\/(en|de)$/)) {
newPath = `/${lang}`;
} else {
newPath = `/${lang}${currentPath}`;
}
const newUrl = newPath + window.location.search + window.location.hash;
window.location.href = newUrl;
};
// Apply translations to all elements with data-i18n attribute
export const applyTranslations = (): void => {
document.querySelectorAll('[data-i18n]').forEach((element) => {
const key = element.getAttribute('data-i18n');
if (key) {
const translation = t(key);
if (translation && translation !== key) {
element.textContent = translation;
}
}
});
document.querySelectorAll('[data-i18n-placeholder]').forEach((element) => {
const key = element.getAttribute('data-i18n-placeholder');
if (key && element instanceof HTMLInputElement) {
const translation = t(key);
if (translation && translation !== key) {
element.placeholder = translation;
}
}
});
document.querySelectorAll('[data-i18n-title]').forEach((element) => {
const key = element.getAttribute('data-i18n-title');
if (key) {
const translation = t(key);
if (translation && translation !== key) {
(element as HTMLElement).title = translation;
}
}
});
document.documentElement.lang = i18next.language;
};
export const rewriteLinks = (): void => {
const currentLang = getLanguageFromUrl();
if (currentLang === 'en') return;
const links = document.querySelectorAll('a[href]');
links.forEach((link) => {
const href = link.getAttribute('href');
if (!href) return;
if (href.startsWith('http') ||
href.startsWith('mailto:') ||
href.startsWith('tel:') ||
href.startsWith('#') ||
href.startsWith('javascript:')) {
return;
}
if (href.match(/^\/(en|de)\//)) {
return;
}
let newHref: string;
if (href.startsWith('/')) {
newHref = `/${currentLang}${href}`;
} else if (href.startsWith('./')) {
newHref = href.replace('./', `/${currentLang}/`);
} else if (href === '/' || href === '') {
newHref = `/${currentLang}/`;
} else {
newHref = `/${currentLang}/${href}`;
}
link.setAttribute('href', newHref);
});
};
export default i18next;

3
src/js/i18n/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { initI18n, t, changeLanguage, applyTranslations, rewriteLinks, getLanguageFromUrl, supportedLanguages, languageNames } from './i18n';
export type { SupportedLanguage } from './i18n';
export { createLanguageSwitcher, injectLanguageSwitcher } from './language-switcher';

View File

@@ -0,0 +1,142 @@
import {
supportedLanguages,
languageNames,
getLanguageFromUrl,
changeLanguage,
} from './i18n';
export const createLanguageSwitcher = (): HTMLElement => {
const currentLang = getLanguageFromUrl();
const container = document.createElement('div');
container.className = 'relative';
container.id = 'language-switcher';
const button = document.createElement('button');
button.className = `
inline-flex items-center gap-1.5 text-sm font-medium
bg-gray-800 text-gray-200 border border-gray-600
px-3 py-1.5 rounded-full transition-colors duration-200
shadow-sm hover:shadow-md hover:bg-gray-700
`.trim();
button.setAttribute('aria-haspopup', 'true');
button.setAttribute('aria-expanded', 'false');
const textSpan = document.createElement('span');
textSpan.className = 'font-medium';
textSpan.textContent = languageNames[currentLang];
const chevron = document.createElement('svg');
chevron.className = 'w-4 h-4';
chevron.setAttribute('fill', 'none');
chevron.setAttribute('stroke', 'currentColor');
chevron.setAttribute('viewBox', '0 0 24 24');
chevron.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>';
button.appendChild(textSpan);
button.appendChild(chevron);
const dropdown = document.createElement('div');
dropdown.className = `
hidden absolute right-0 mt-2 w-40 rounded-lg
bg-gray-800 border border-gray-700 shadow-xl
py-1 z-50
`.trim();
dropdown.setAttribute('role', 'menu');
supportedLanguages.forEach((lang) => {
const option = document.createElement('button');
option.className = `
w-full px-4 py-2 text-left text-sm text-gray-200
hover:bg-gray-700 flex items-center gap-2
${lang === currentLang ? 'bg-gray-700' : ''}
`.trim();
option.setAttribute('role', 'menuitem');
const name = document.createElement('span');
name.textContent = languageNames[lang];
option.appendChild(name);
option.addEventListener('click', () => {
if (lang !== currentLang) {
changeLanguage(lang);
}
});
dropdown.appendChild(option);
});
container.appendChild(button);
container.appendChild(dropdown);
button.addEventListener('click', (e) => {
e.stopPropagation();
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', (!isExpanded).toString());
dropdown.classList.toggle('hidden');
});
document.addEventListener('click', () => {
button.setAttribute('aria-expanded', 'false');
dropdown.classList.add('hidden');
});
return container;
};
export const injectLanguageSwitcher = (): void => {
const footer = document.querySelector('footer');
if (!footer) return;
const headings = footer.querySelectorAll('h3');
let followUsColumn: HTMLElement | null = null;
headings.forEach((h3) => {
if (h3.textContent?.trim() === 'Follow Us' || h3.textContent?.trim() === 'Folgen Sie uns') {
followUsColumn = h3.parentElement;
}
});
if (followUsColumn) {
const socialIconsContainer = followUsColumn.querySelector('.space-x-4');
if (socialIconsContainer) {
const wrapper = document.createElement('div');
wrapper.className = 'inline-flex flex-col gap-4'; // gap-4 adds space between icons and switcher
socialIconsContainer.parentNode?.insertBefore(wrapper, socialIconsContainer);
wrapper.appendChild(socialIconsContainer);
const switcher = createLanguageSwitcher();
switcher.className = 'relative w-full';
const button = switcher.querySelector('button');
if (button) {
button.className = `
flex items-center justify-between w-full text-sm font-medium
bg-gray-800 text-gray-400 border border-gray-700
px-3 py-2 rounded-lg transition-colors duration-200
hover:text-white hover:border-gray-600
`.trim();
}
const dropdown = switcher.querySelector('div[role="menu"]');
if (dropdown) {
dropdown.classList.remove('mt-2', 'w-40');
dropdown.classList.add('bottom-full', 'mb-2', 'w-full');
}
wrapper.appendChild(switcher);
} else {
const switcherContainer = document.createElement('div');
switcherContainer.className = 'mt-4 w-full';
const switcher = createLanguageSwitcher();
switcherContainer.appendChild(switcher);
followUsColumn.appendChild(switcherContainer);
}
}
};

View File

@@ -0,0 +1,356 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
const worker = new Worker(import.meta.env.BASE_URL + 'workers/add-attachments.worker.js');
interface AddAttachmentState {
file: File | null;
pdfDoc: PDFLibDocument | null;
attachments: File[];
}
const pageState: AddAttachmentState = {
file: null,
pdfDoc: null,
attachments: [],
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
pageState.attachments = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const attachmentFileList = document.getElementById('attachment-file-list');
if (attachmentFileList) attachmentFileList.innerHTML = '';
const attachmentInput = document.getElementById('attachment-files-input') as HTMLInputElement;
if (attachmentInput) attachmentInput.value = '';
const attachmentLevelOptions = document.getElementById('attachment-level-options');
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
if (pageRangeWrapper) pageRangeWrapper.classList.add('hidden');
const processBtn = document.getElementById('process-btn');
if (processBtn) processBtn.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const documentRadio = document.querySelector('input[name="attachment-level"][value="document"]') as HTMLInputElement;
if (documentRadio) documentRadio.checked = true;
}
worker.onmessage = function (e) {
const data = e.data;
if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`${originalName}_with_attachments.pdf`
);
showAlert('Success', `${pageState.attachments.length} file(s) attached successfully.`, 'success', function () {
resetState();
});
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
}
};
worker.onerror = function (error) {
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
};
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
const totalPagesSpan = document.getElementById('attachment-total-pages');
if (totalPagesSpan) totalPagesSpan.textContent = pageCount.toString();
hideLoader();
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function updateAttachmentList() {
const attachmentFileList = document.getElementById('attachment-file-list');
const attachmentLevelOptions = document.getElementById('attachment-level-options');
const processBtn = document.getElementById('process-btn');
if (!attachmentFileList) return;
attachmentFileList.innerHTML = '';
pageState.attachments.forEach(function (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 = formatBytes(file.size);
div.append(nameSpan, sizeSpan);
attachmentFileList.appendChild(div);
});
if (pageState.attachments.length > 0) {
if (attachmentLevelOptions) attachmentLevelOptions.classList.remove('hidden');
if (processBtn) processBtn.classList.remove('hidden');
} else {
if (attachmentLevelOptions) attachmentLevelOptions.classList.add('hidden');
if (processBtn) processBtn.classList.add('hidden');
}
}
async function addAttachments() {
if (!pageState.file || !pageState.pdfDoc) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
if (pageState.attachments.length === 0) {
showAlert('No Files', 'Please select at least one file to attach.');
return;
}
const attachmentLevel = (
document.querySelector('input[name="attachment-level"]:checked') as HTMLInputElement
)?.value || 'document';
let pageRange: string = '';
if (attachmentLevel === 'page') {
const pageRangeInput = document.getElementById('attachment-page-range') as HTMLInputElement;
pageRange = pageRangeInput?.value?.trim() || '';
if (!pageRange) {
showAlert('Error', 'Please specify a page range for page-level attachments.');
return;
}
}
showLoader('Embedding files into PDF...');
try {
const pdfBuffer = await pageState.file.arrayBuffer();
const attachmentBuffers: ArrayBuffer[] = [];
const attachmentNames: string[] = [];
for (let i = 0; i < pageState.attachments.length; i++) {
const file = pageState.attachments[i];
showLoader(`Reading ${file.name} (${i + 1}/${pageState.attachments.length})...`);
const fileBuffer = await file.arrayBuffer();
attachmentBuffers.push(fileBuffer);
attachmentNames.push(file.name);
}
showLoader('Attaching files to PDF...');
const message = {
command: 'add-attachments',
pdfBuffer: pdfBuffer,
attachmentBuffers: attachmentBuffers,
attachmentNames: attachmentNames,
attachmentLevel: attachmentLevel,
pageRange: pageRange
};
const transferables = [pdfBuffer, ...attachmentBuffers];
worker.postMessage(message, transferables);
} catch (error: any) {
console.error('Error attaching files:', error);
hideLoader();
showAlert('Error', `Failed to attach files: ${error.message}`);
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
function handleAttachmentSelect(files: FileList | null) {
if (files && files.length > 0) {
pageState.attachments = Array.from(files);
updateAttachmentList();
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const attachmentInput = document.getElementById('attachment-files-input') as HTMLInputElement;
const attachmentDropZone = document.getElementById('attachment-drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (attachmentInput && attachmentDropZone) {
attachmentInput.addEventListener('change', function (e) {
handleAttachmentSelect((e.target as HTMLInputElement).files);
});
attachmentDropZone.addEventListener('dragover', function (e) {
e.preventDefault();
attachmentDropZone.classList.add('bg-gray-700');
});
attachmentDropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
attachmentDropZone.classList.remove('bg-gray-700');
});
attachmentDropZone.addEventListener('drop', function (e) {
e.preventDefault();
attachmentDropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files) {
handleAttachmentSelect(files);
}
});
attachmentInput.addEventListener('click', function () {
attachmentInput.value = '';
});
}
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
attachmentLevelRadios.forEach(function (radio) {
radio.addEventListener('change', function (e) {
const value = (e.target as HTMLInputElement).value;
if (value === 'page' && pageRangeWrapper) {
pageRangeWrapper.classList.remove('hidden');
} else if (pageRangeWrapper) {
pageRangeWrapper.classList.add('hidden');
}
});
});
if (processBtn) {
processBtn.addEventListener('click', addAttachments);
}
});

View File

@@ -1,212 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui';
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
import { state } from '../state';
const worker = new Worker(import.meta.env.BASE_URL + 'workers/add-attachments.worker.js');
let attachments: File[] = [];
worker.onmessage = (e) => {
const data = e.data;
if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`attached-${state.files[0].name}`
);
showAlert('Success', `${attachments.length} file(s) attached successfully.`);
clearAttachments();
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
clearAttachments();
}
};
worker.onerror = (error) => {
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
clearAttachments();
};
export async function addAttachments() {
if (!state.files || state.files.length === 0) {
showAlert('Error', 'Main PDF is not loaded.');
return;
}
if (attachments.length === 0) {
showAlert('No Files', 'Please select at least one file to attach.');
return;
}
const attachmentLevel = (
document.querySelector('input[name="attachment-level"]:checked') as HTMLInputElement
)?.value || 'document';
let pageRange: string = '';
if (attachmentLevel === 'page') {
const pageRangeInput = document.getElementById('attachment-page-range') as HTMLInputElement;
pageRange = pageRangeInput?.value?.trim() || '';
if (!pageRange) {
showAlert('Error', 'Please specify a page range for page-level attachments.');
return;
}
}
showLoader('Embedding files into PDF...');
try {
const pdfFile = state.files[0];
const pdfBuffer = (await readFileAsArrayBuffer(pdfFile)) as ArrayBuffer;
const attachmentBuffers: ArrayBuffer[] = [];
const attachmentNames: string[] = [];
for (let i = 0; i < attachments.length; i++) {
const file = attachments[i];
showLoader(`Reading ${file.name} (${i + 1}/${attachments.length})...`);
const fileBuffer = (await readFileAsArrayBuffer(file)) as ArrayBuffer;
attachmentBuffers.push(fileBuffer);
attachmentNames.push(file.name);
}
showLoader('Attaching files to PDF...');
const message = {
command: 'add-attachments',
pdfBuffer: pdfBuffer,
attachmentBuffers: attachmentBuffers,
attachmentNames: attachmentNames,
attachmentLevel: attachmentLevel,
pageRange: pageRange
};
const transferables = [pdfBuffer, ...attachmentBuffers];
worker.postMessage(message, transferables);
} catch (error: any) {
console.error('Error attaching files:', error);
hideLoader();
showAlert('Error', `Failed to attach files: ${error.message}`);
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;
const attachmentLevelOptions = document.getElementById('attachment-level-options');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
if (fileListDiv) fileListDiv.innerHTML = '';
if (attachmentInput) attachmentInput.value = '';
if (processBtn) {
processBtn.disabled = true;
processBtn.classList.add('hidden');
}
if (attachmentLevelOptions) {
attachmentLevelOptions.classList.add('hidden');
}
if (pageRangeWrapper) {
pageRangeWrapper.classList.add('hidden');
}
const documentRadio = document.querySelector('input[name="attachment-level"][value="document"]') as HTMLInputElement;
if (documentRadio) {
documentRadio.checked = true;
}
}
export function setupAddAttachmentsTool() {
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;
const attachmentLevelOptions = document.getElementById('attachment-level-options');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
const totalPagesSpan = document.getElementById('attachment-total-pages');
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
console.error('Attachment tool UI elements not found.');
return;
}
if (!state.files || state.files.length === 0) {
console.error('No PDF file loaded for adding attachments.');
return;
}
optionsDiv.classList.remove('hidden');
if (totalPagesSpan && state.pdfDoc) {
totalPagesSpan.textContent = state.pdfDoc.getPageCount().toString();
}
if (attachmentInput.dataset.listenerAttached) return;
attachmentInput.dataset.listenerAttached = 'true';
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);
});
if (attachmentLevelOptions) {
attachmentLevelOptions.classList.remove('hidden');
}
processBtn.disabled = false;
processBtn.classList.remove('hidden');
} else {
clearAttachments();
}
});
const attachmentLevelRadios = document.querySelectorAll('input[name="attachment-level"]');
attachmentLevelRadios.forEach((radio) => {
radio.addEventListener('change', (e) => {
const value = (e.target as HTMLInputElement).value;
if (value === 'page' && pageRangeWrapper) {
pageRangeWrapper.classList.remove('hidden');
} else if (pageRangeWrapper) {
pageRangeWrapper.classList.add('hidden');
}
});
});
processBtn.onclick = addAttachments;
}

View File

@@ -0,0 +1,234 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
interface AddBlankPageState {
file: File | null;
pdfDoc: PDFLibDocument | null;
}
const pageState: AddBlankPageState = {
file: null,
pdfDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
if (pagePositionInput) pagePositionInput.value = '0';
const pageCountInput = document.getElementById('page-count') as HTMLInputElement;
if (pageCountInput) pageCountInput.value = '1';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const pagePositionHint = document.getElementById('page-position-hint');
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
// Load PDF document
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
if (pagePositionHint) {
pagePositionHint.textContent = `Enter 0 to insert at the beginning, or ${pageCount} to insert at the end.`;
}
if (pagePositionInput) {
pagePositionInput.max = pageCount.toString();
}
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function addBlankPages() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
const pagePositionInput = document.getElementById('page-position') as HTMLInputElement;
const pageCountInput = document.getElementById('page-count') as HTMLInputElement;
const position = parseInt(pagePositionInput.value);
const insertCount = parseInt(pageCountInput.value);
const totalPages = pageState.pdfDoc.getPageCount();
if (isNaN(position) || position < 0 || position > totalPages) {
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
return;
}
if (isNaN(insertCount) || insertCount < 1) {
showAlert('Invalid Input', 'Please enter a valid number of pages (1 or more).');
return;
}
showLoader(`Adding ${insertCount} blank page${insertCount > 1 ? 's' : ''}...`);
try {
const newPdf = await PDFLibDocument.create();
const { width, height } = pageState.pdfDoc.getPage(0).getSize();
const allIndices = Array.from({ length: totalPages }, function (_, i) { return i; });
const indicesBefore = allIndices.slice(0, position);
const indicesAfter = allIndices.slice(position);
if (indicesBefore.length > 0) {
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesBefore);
copied.forEach(function (p) { newPdf.addPage(p); });
}
// Add the specified number of blank pages
for (let i = 0; i < insertCount; i++) {
newPdf.addPage([width, height]);
}
if (indicesAfter.length > 0) {
const copied = await newPdf.copyPages(pageState.pdfDoc, indicesAfter);
copied.forEach(function (p) { newPdf.addPage(p); });
}
const newPdfBytes = await newPdf.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_blank-pages-added.pdf`
);
showAlert('Success', `Added ${insertCount} blank page${insertCount > 1 ? 's' : ''} successfully!`, 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', `Could not add blank page${insertCount > 1 ? 's' : ''}.`);
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', addBlankPages);
}
});

View File

@@ -1,76 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function addBlankPage() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageNumberInput = document.getElementById('page-number').value;
// @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;
if (pageNumberInput.trim() === '') {
showAlert('Invalid Input', 'Please enter a page number.');
return;
}
if (pageCountInput.trim() === '') {
showAlert('Invalid Input', 'Please enter the number of pages to insert.');
return;
}
const position = parseInt(pageNumberInput);
const pageCount = parseInt(pageCountInput);
const totalPages = state.pdfDoc.getPageCount();
if (isNaN(position) || position < 0 || position > totalPages) {
showAlert(
'Invalid Input',
`Please enter a number between 0 and ${totalPages}.`
);
return;
}
if (isNaN(pageCount) || pageCount < 1) {
showAlert(
'Invalid Input',
'Please enter a valid number of pages (1 or more).'
);
return;
}
showLoader(`Adding ${pageCount} blank page${pageCount > 1 ? 's' : ''}...`);
try {
const newPdf = await PDFLibDocument.create();
const { width, height } = state.pdfDoc.getPage(0).getSize();
const allIndices = Array.from({ length: totalPages }, (_, i) => i);
const indicesBefore = allIndices.slice(0, position);
const indicesAfter = allIndices.slice(position);
if (indicesBefore.length > 0) {
const copied = await newPdf.copyPages(state.pdfDoc, indicesBefore);
copied.forEach((p: any) => newPdf.addPage(p));
}
// Add the specified number of blank pages
for (let i = 0; i < pageCount; i++) {
newPdf.addPage([width, height]);
}
if (indicesAfter.length > 0) {
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
copied.forEach((p: any) => newPdf.addPage(p));
}
const newPdfBytes = await newPdf.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`blank-page${pageCount > 1 ? 's' : ''}-added.pdf`
);
} catch (e) {
console.error(e);
showAlert('Error', `Could not add blank page${pageCount > 1 ? 's' : ''}.`);
} finally {
hideLoader();
}
}

View File

@@ -1,157 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb, parsePageRanges } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
export function setupHeaderFooterUI() {
const totalPagesSpan = document.getElementById('total-pages');
if (totalPagesSpan && state.pdfDoc) {
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
}
}
export async function addHeaderFooter() {
showLoader('Adding header & footer...');
try {
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
const allPages = state.pdfDoc.getPages();
const totalPages = allPages.length;
const margin = 40;
// --- 1. Get new formatting options from the UI ---
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('font-color').value;
const fontColor = hexToRgb(colorHex);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageRangeInput = document.getElementById('page-range').value;
// --- 2. Get text values ---
const texts = {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerLeft: document.getElementById('header-left').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerCenter: document.getElementById('header-center').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerRight: document.getElementById('header-right').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerLeft: document.getElementById('footer-left').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerCenter: document.getElementById('footer-center').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerRight: document.getElementById('footer-right').value,
};
// --- 3. Parse page range to determine which pages to modify ---
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (indicesToProcess.length === 0) {
throw new Error(
"Invalid page range specified. Please check your input (e.g., '1-3, 5')."
);
}
// --- 4. Define drawing options with new values ---
const drawOptions = {
font: helveticaFont,
size: fontSize,
color: rgb(fontColor.r, fontColor.g, fontColor.b),
};
// --- 5. Loop over only the selected pages ---
for (const pageIndex of indicesToProcess) {
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
const page = allPages[pageIndex];
const { width, height } = page.getSize();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
const pageNumber = pageIndex + 1; // For dynamic text
// Helper to replace placeholders like {page} and {total}
const processText = (text: any) =>
text.replace(/{page}/g, pageNumber).replace(/{total}/g, totalPages);
// Get processed text for the current page
const processedTexts = {
headerLeft: processText(texts.headerLeft),
headerCenter: processText(texts.headerCenter),
headerRight: processText(texts.headerRight),
footerLeft: processText(texts.footerLeft),
footerCenter: processText(texts.footerCenter),
footerRight: processText(texts.footerRight),
};
if (processedTexts.headerLeft)
page.drawText(processedTexts.headerLeft, {
...drawOptions,
x: margin,
y: height - margin,
});
if (processedTexts.headerCenter)
page.drawText(processedTexts.headerCenter, {
...drawOptions,
x:
width / 2 -
helveticaFont.widthOfTextAtSize(
processedTexts.headerCenter,
fontSize
) /
2,
y: height - margin,
});
if (processedTexts.headerRight)
page.drawText(processedTexts.headerRight, {
...drawOptions,
x:
width -
margin -
helveticaFont.widthOfTextAtSize(
processedTexts.headerRight,
fontSize
),
y: height - margin,
});
if (processedTexts.footerLeft)
page.drawText(processedTexts.footerLeft, {
...drawOptions,
x: margin,
y: margin,
});
if (processedTexts.footerCenter)
page.drawText(processedTexts.footerCenter, {
...drawOptions,
x:
width / 2 -
helveticaFont.widthOfTextAtSize(
processedTexts.footerCenter,
fontSize
) /
2,
y: margin,
});
if (processedTexts.footerRight)
page.drawText(processedTexts.footerRight, {
...drawOptions,
x:
width -
margin -
helveticaFont.widthOfTextAtSize(
processedTexts.footerRight,
fontSize
),
y: margin,
});
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'header-footer-added.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Could not add header or footer.');
} finally {
hideLoader();
}
}

View File

@@ -1,137 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { rgb, StandardFonts } from 'pdf-lib';
export async function addPageNumbers() {
showLoader('Adding page numbers...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const position = document.getElementById('position').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const format = document.getElementById('number-format').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color').value;
const textColor = hexToRgb(colorHex);
const pages = state.pdfDoc.getPages();
const totalPages = pages.length;
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
for (let i = 0; i < totalPages; i++) {
const page = pages[i];
const mediaBox = page.getMediaBox();
const cropBox = page.getCropBox();
const bounds = cropBox || mediaBox;
const width = bounds.width;
const height = bounds.height;
const xOffset = bounds.x || 0;
const yOffset = bounds.y || 0;
let pageNumText =
format === 'page_x_of_y' ? `${i + 1} / ${totalPages}` : `${i + 1}`;
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
const textHeight = fontSize;
const minMargin = 8;
const maxMargin = 40;
const marginPercentage = 0.04;
const horizontalMargin = Math.max(
minMargin,
Math.min(maxMargin, width * marginPercentage)
);
const verticalMargin = Math.max(
minMargin,
Math.min(maxMargin, height * marginPercentage)
);
// Ensure text doesn't go outside visible page boundaries
const safeHorizontalMargin = Math.max(
horizontalMargin,
textWidth / 2 + 3
);
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
let x, y;
switch (position) {
case 'bottom-center':
x =
Math.max(
safeHorizontalMargin,
Math.min(
width - safeHorizontalMargin - textWidth,
(width - textWidth) / 2
)
) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-left':
x = safeHorizontalMargin + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-right':
x =
Math.max(
safeHorizontalMargin,
width - safeHorizontalMargin - textWidth
) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'top-center':
x =
Math.max(
safeHorizontalMargin,
Math.min(
width - safeHorizontalMargin - textWidth,
(width - textWidth) / 2
)
) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-left':
x = safeHorizontalMargin + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-right':
x =
Math.max(
safeHorizontalMargin,
width - safeHorizontalMargin - textWidth
) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
}
// Final safety check to ensure coordinates are within visible page bounds
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
page.drawText(pageNumText, {
x,
y,
font: helveticaFont,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b),
});
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'paginated.pdf'
);
showAlert('Success', 'Page numbers added successfully!');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not add page numbers.');
} finally {
hideLoader();
}
}

View File

@@ -1,5 +1,6 @@
import { formatBytes, readFileAsArrayBuffer } from '../utils/helpers'
import { formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers'
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'
import { createIcons, icons } from 'lucide'
let selectedFile: File | null = null
let viewerIframe: HTMLIFrameElement | null = null
@@ -12,6 +13,37 @@ const viewerContainer = document.getElementById('stamp-viewer-container') as HTM
const viewerCard = document.getElementById('viewer-card') as HTMLDivElement | null
const saveStampedBtn = document.getElementById('save-stamped-btn') as HTMLButtonElement
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement | null
const toolUploader = document.getElementById('tool-uploader') as HTMLDivElement | null
function resetState() {
selectedFile = null
if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl)
currentBlobUrl = null
}
if (viewerIframe && viewerContainer && viewerIframe.parentElement === viewerContainer) {
viewerContainer.removeChild(viewerIframe)
}
viewerIframe = null
viewerReady = false
if (viewerCard) viewerCard.classList.add('hidden')
if (saveStampedBtn) saveStampedBtn.classList.add('hidden')
if (viewerContainer) {
viewerContainer.style.height = ''
viewerContainer.style.aspectRatio = ''
}
// Revert container width only if NOT in full width mode
const isFullWidth = localStorage.getItem('fullWidthMode') === 'true'
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-6xl')
toolUploader.classList.add('max-w-2xl')
}
updateFileList()
if (pdfInput) pdfInput.value = ''
}
function updateFileList() {
if (!selectedFile) {
@@ -23,19 +55,67 @@ function updateFileList() {
fileListDiv.classList.remove('hidden')
fileListDiv.innerHTML = ''
const wrapper = document.createElement('div')
wrapper.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg mb-2'
// Expand container width for viewer if NOT in full width mode
const isFullWidth = localStorage.getItem('fullWidthMode') === 'true'
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-2xl')
toolUploader.classList.add('max-w-6xl')
}
const nameSpan = document.createElement('span')
nameSpan.className = 'truncate font-medium text-gray-200'
const wrapper = document.createElement('div')
wrapper.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors'
const innerDiv = document.createElement('div')
innerDiv.className = 'flex items-center justify-between'
const infoDiv = document.createElement('div')
infoDiv.className = 'flex-1 min-w-0'
const nameSpan = document.createElement('p')
nameSpan.className = 'truncate font-medium text-white'
nameSpan.textContent = selectedFile.name
const sizeSpan = document.createElement('span')
sizeSpan.className = 'ml-3 text-gray-400 text-xs flex-shrink-0'
const sizeSpan = document.createElement('p')
sizeSpan.className = 'text-gray-400 text-sm'
sizeSpan.textContent = formatBytes(selectedFile.size)
wrapper.append(nameSpan, sizeSpan)
infoDiv.append(nameSpan, sizeSpan)
const deleteBtn = document.createElement('button')
deleteBtn.className = 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2'
deleteBtn.title = 'Remove file'
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>'
deleteBtn.onclick = (e) => {
e.stopPropagation()
resetState()
}
innerDiv.append(infoDiv, deleteBtn)
wrapper.appendChild(innerDiv)
fileListDiv.appendChild(wrapper)
createIcons({ icons })
}
async function adjustViewerHeight(file: File) {
if (!viewerContainer) return
try {
const arrayBuffer = await file.arrayBuffer()
const loadingTask = getPDFDocument({ data: arrayBuffer })
const pdf = await loadingTask.promise
const page = await pdf.getPage(1)
const viewport = page.getViewport({ scale: 1 })
// Add ~50px for toolbar height relative to page height
const aspectRatio = viewport.width / (viewport.height + 50)
viewerContainer.style.height = 'auto'
viewerContainer.style.aspectRatio = `${aspectRatio}`
} catch (e) {
console.error('Error adjusting viewer height:', e)
// Fallback if calculation fails
viewerContainer.style.height = '70vh'
}
}
async function loadPdfInViewer(file: File) {
@@ -55,6 +135,10 @@ async function loadPdfInViewer(file: File) {
}
viewerIframe = null
viewerReady = false
// Calculate and apply dynamic height
await adjustViewerHeight(file)
const arrayBuffer = await readFileAsArrayBuffer(file)
const blob = new Blob([arrayBuffer as BlobPart], { type: 'application/pdf' })
currentBlobUrl = URL.createObjectURL(blob)
@@ -128,6 +212,7 @@ function setupAnnotationViewer(iframe: HTMLIFrameElement) {
async function onPdfSelected(file: File) {
selectedFile = file
updateFileList()
if (saveStampedBtn) saveStampedBtn.classList.remove('hidden')
await loadPdfInViewer(file)
}
@@ -141,6 +226,26 @@ if (pdfInput) {
})
}
// Add drag/drop support
const dropZone = document.getElementById('drop-zone')
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault()
dropZone.classList.add('border-indigo-500')
})
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500')
})
dropZone.addEventListener('drop', async (e) => {
e.preventDefault()
dropZone.classList.remove('border-indigo-500')
const file = e.dataTransfer?.files[0]
if (file && file.type === 'application/pdf') {
await onPdfSelected(file)
}
})
}
if (saveStampedBtn) {
saveStampedBtn.addEventListener('click', () => {
if (!viewerIframe) {
@@ -155,7 +260,10 @@ if (saveStampedBtn) {
if (extensionInstance && typeof extensionInstance.exportPdf === 'function') {
const result = extensionInstance.exportPdf()
if (result && typeof result.then === 'function') {
result.catch((err: unknown) => {
result.then(() => {
// Reset state after successful export
setTimeout(() => resetState(), 500)
}).catch((err: unknown) => {
console.error('Error while exporting stamped PDF via annotation extension:', err)
})
}

View File

@@ -0,0 +1,220 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, hexToRgb, formatBytes, readFileAsArrayBuffer } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb, degrees, StandardFonts } from 'pdf-lib';
interface PageState {
file: File | null;
pdfDoc: PDFLibDocument | null;
}
const pageState: PageState = { file: null, pdfDoc: null };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; });
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
});
}
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
if (processBtn) processBtn.addEventListener('click', addWatermark);
setupWatermarkUI();
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files?.length) handleFiles(input.files);
}
async function handleFiles(files: FileList) {
const file = files[0];
if (!file || file.type !== 'application/pdf') {
showAlert('Invalid File', 'Please upload a valid PDF file.');
return;
}
showLoader('Loading PDF...');
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
pageState.file = file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) {
console.error(error);
showAlert('Error', 'Failed to load PDF file.');
} finally {
hideLoader();
}
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.pdfDoc.getPageCount()} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
function setupWatermarkUI() {
const watermarkTypeRadios = document.querySelectorAll('input[name="watermark-type"]');
const textOptions = document.getElementById('text-watermark-options');
const imageOptions = document.getElementById('image-watermark-options');
watermarkTypeRadios.forEach((radio) => {
radio.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
if (target.value === 'text') {
textOptions?.classList.remove('hidden');
imageOptions?.classList.add('hidden');
} else {
textOptions?.classList.add('hidden');
imageOptions?.classList.remove('hidden');
}
});
});
const opacitySliderText = document.getElementById('opacity-text') as HTMLInputElement;
const opacityValueText = document.getElementById('opacity-value-text');
const angleSliderText = document.getElementById('angle-text') as HTMLInputElement;
const angleValueText = document.getElementById('angle-value-text');
opacitySliderText?.addEventListener('input', () => { if (opacityValueText) opacityValueText.textContent = opacitySliderText.value; });
angleSliderText?.addEventListener('input', () => { if (angleValueText) angleValueText.textContent = angleSliderText.value; });
const opacitySliderImage = document.getElementById('opacity-image') as HTMLInputElement;
const opacityValueImage = document.getElementById('opacity-value-image');
const angleSliderImage = document.getElementById('angle-image') as HTMLInputElement;
const angleValueImage = document.getElementById('angle-value-image');
opacitySliderImage?.addEventListener('input', () => { if (opacityValueImage) opacityValueImage.textContent = opacitySliderImage.value; });
angleSliderImage?.addEventListener('input', () => { if (angleValueImage) angleValueImage.textContent = angleSliderImage.value; });
}
async function addWatermark() {
if (!pageState.pdfDoc) {
showAlert('Error', 'Please upload a PDF file first.');
return;
}
const watermarkType = (document.querySelector('input[name="watermark-type"]:checked') as HTMLInputElement)?.value || 'text';
showLoader('Adding watermark...');
try {
const pages = pageState.pdfDoc.getPages();
let watermarkAsset: any = null;
if (watermarkType === 'text') {
watermarkAsset = await pageState.pdfDoc.embedFont(StandardFonts.Helvetica);
} else {
const imageFile = (document.getElementById('image-watermark-input') as HTMLInputElement).files?.[0];
if (!imageFile) throw new Error('Please select an image file for the watermark.');
const imageBytes = await readFileAsArrayBuffer(imageFile);
if (imageFile.type === 'image/png') {
watermarkAsset = await pageState.pdfDoc.embedPng(imageBytes as ArrayBuffer);
} else if (imageFile.type === 'image/jpeg') {
watermarkAsset = await pageState.pdfDoc.embedJpg(imageBytes as ArrayBuffer);
} else {
throw new Error('Unsupported Image. Please use a PNG or JPG for the watermark.');
}
}
for (const page of pages) {
const { width, height } = page.getSize();
if (watermarkType === 'text') {
const text = (document.getElementById('watermark-text') as HTMLInputElement).value;
if (!text.trim()) throw new Error('Please enter text for the watermark.');
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement).value) || 72;
const angle = parseInt((document.getElementById('angle-text') as HTMLInputElement).value) || 0;
const opacity = parseFloat((document.getElementById('opacity-text') as HTMLInputElement).value) || 0.3;
const colorHex = (document.getElementById('text-color') as HTMLInputElement).value;
const textColor = hexToRgb(colorHex);
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
page.drawText(text, {
x: (width - textWidth) / 2,
y: height / 2,
font: watermarkAsset,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b),
opacity: opacity,
rotate: degrees(angle),
});
} else {
const angle = parseInt((document.getElementById('angle-image') as HTMLInputElement).value) || 0;
const opacity = parseFloat((document.getElementById('opacity-image') as HTMLInputElement).value) || 0.3;
const scale = 0.5;
const imgWidth = watermarkAsset.width * scale;
const imgHeight = watermarkAsset.height * scale;
page.drawImage(watermarkAsset, {
x: (width - imgWidth) / 2,
y: (height - imgHeight) / 2,
width: imgWidth,
height: imgHeight,
opacity: opacity,
rotate: degrees(angle),
});
}
}
const newPdfBytes = await pageState.pdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'watermarked.pdf');
showAlert('Success', 'Watermark added successfully!', 'success', () => { resetState(); });
} catch (e: any) {
console.error(e);
showAlert('Error', e.message || 'Could not add the watermark.');
} finally {
hideLoader();
}
}

View File

@@ -1,189 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
readFileAsArrayBuffer,
hexToRgb,
resetAndReloadTool,
} from '../utils/helpers.js';
import { state, resetState } from '../state.js';
import {
PDFDocument as PDFLibDocument,
rgb,
degrees,
StandardFonts,
} from 'pdf-lib';
export function setupWatermarkUI() {
const watermarkTypeRadios = document.querySelectorAll(
'input[name="watermark-type"]'
);
const textOptions = document.getElementById('text-watermark-options');
const imageOptions = document.getElementById('image-watermark-options');
watermarkTypeRadios.forEach((radio) => {
radio.addEventListener('change', (e) => {
if (e.target.value === 'text') {
textOptions.classList.remove('hidden');
imageOptions.classList.add('hidden');
} else {
textOptions.classList.add('hidden');
imageOptions.classList.remove('hidden');
}
});
});
const opacitySliderText = document.getElementById('opacity-text');
const opacityValueText = document.getElementById('opacity-value-text');
const angleSliderText = document.getElementById('angle-text');
const angleValueText = document.getElementById('angle-value-text');
opacitySliderText.addEventListener(
'input',
() =>
(opacityValueText.textContent = (
opacitySliderText as HTMLInputElement
).value)
);
angleSliderText.addEventListener(
'input',
() =>
(angleValueText.textContent = (angleSliderText as HTMLInputElement).value)
);
const opacitySliderImage = document.getElementById('opacity-image');
const opacityValueImage = document.getElementById('opacity-value-image');
const angleSliderImage = document.getElementById('angle-image');
const angleValueImage = document.getElementById('angle-value-image');
opacitySliderImage.addEventListener(
'input',
() =>
(opacityValueImage.textContent = (
opacitySliderImage as HTMLInputElement
).value)
);
angleSliderImage.addEventListener(
'input',
() =>
(angleValueImage.textContent = (
angleSliderImage as HTMLInputElement
).value)
);
}
export async function addWatermark() {
const watermarkType = (
document.querySelector(
'input[name="watermark-type"]:checked'
) as HTMLInputElement
).value;
showLoader('Adding watermark...');
try {
const pages = state.pdfDoc.getPages();
let watermarkAsset = null;
if (watermarkType === 'text') {
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
} else {
// 'image'
const imageFile = (
document.getElementById('image-watermark-input') as HTMLInputElement
).files?.[0];
if (!imageFile)
throw new Error('Please select an image file for the watermark.');
const imageBytes = await readFileAsArrayBuffer(imageFile);
if (imageFile.type === 'image/png') {
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
} else if (imageFile.type === 'image/jpeg') {
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
} else {
throw new Error(
'Unsupported Image. Please use a PNG or JPG for the watermark.'
);
}
}
for (const page of pages) {
const { width, height } = page.getSize();
if (watermarkType === 'text') {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const text = document.getElementById('watermark-text').value;
if (!text.trim())
throw new Error('Please enter text for the watermark.');
const fontSize =
parseInt(
(document.getElementById('font-size') as HTMLInputElement).value
) || 72;
const angle =
parseInt(
(document.getElementById('angle-text') as HTMLInputElement).value
) || 0;
const opacity =
parseFloat(
(document.getElementById('opacity-text') as HTMLInputElement).value
) || 0.3;
const colorHex = (
document.getElementById('text-color') as HTMLInputElement
).value;
const textColor = hexToRgb(colorHex);
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
page.drawText(text, {
x: (width - textWidth) / 2,
y: height / 2,
font: watermarkAsset,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b),
opacity: opacity,
rotate: degrees(angle),
});
} else {
const angle =
parseInt(
(document.getElementById('angle-image') as HTMLInputElement).value
) || 0;
const opacity =
parseFloat(
(document.getElementById('opacity-image') as HTMLInputElement).value
) || 0.3;
const scale = 0.5;
const imgWidth = watermarkAsset.width * scale;
const imgHeight = watermarkAsset.height * scale;
page.drawImage(watermarkAsset, {
x: (width - imgWidth) / 2,
y: (height - imgHeight) / 2,
width: imgWidth,
height: imgHeight,
opacity: opacity,
rotate: degrees(angle),
});
}
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'watermarked.pdf'
);
resetAndReloadTool();
} catch (e) {
console.error(e);
showAlert(
'Error',
e.message || 'Could not add the watermark. Please check your inputs.'
);
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,252 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import Sortable from 'sortablejs';
interface AlternateMergeState {
files: File[];
pdfBytes: Map<string, ArrayBuffer>;
pdfDocs: Map<string, any>;
}
const pageState: AlternateMergeState = {
files: [],
pdfBytes: new Map(),
pdfDocs: new Map(),
};
const alternateMergeWorker = new Worker(import.meta.env.BASE_URL + 'workers/alternate-merge.worker.js');
function resetState() {
pageState.files = [];
pageState.pdfBytes.clear();
pageState.pdfDocs.clear();
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const fileList = document.getElementById('file-list');
if (fileList) fileList.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileList = document.getElementById('file-list');
if (!fileDisplayArea || !fileList) return;
fileDisplayArea.innerHTML = '';
if (pageState.files.length > 0) {
// Show file count summary
const summaryDiv = document.createElement('div');
summaryDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoSpan = document.createElement('span');
infoSpan.className = 'text-gray-200';
infoSpan.textContent = `${pageState.files.length} PDF files selected`;
const clearBtn = document.createElement('button');
clearBtn.className = 'text-red-400 hover:text-red-300';
clearBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
clearBtn.onclick = function () {
resetState();
};
summaryDiv.append(infoSpan, clearBtn);
fileDisplayArea.appendChild(summaryDiv);
createIcons({ icons });
// Load PDFs and populate list
showLoader('Loading PDF files...');
fileList.innerHTML = '';
try {
for (const file of pageState.files) {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfBytes.set(file.name, arrayBuffer);
const bytesForPdfJs = arrayBuffer.slice(0);
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
pageState.pdfDocs.set(file.name, pdfjsDoc);
const pageCount = pdfjsDoc.numPages;
const li = document.createElement('li');
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
li.dataset.fileName = file.name;
const infoDiv = document.createElement('div');
infoDiv.className = 'flex items-center gap-2 truncate flex-1';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('span');
metaSpan.className = 'text-sm text-gray-400 flex-shrink-0';
metaSpan.textContent = `${formatBytes(file.size)}${pageCount} pages`;
infoDiv.append(nameSpan, metaSpan);
const dragHandle = document.createElement('div');
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded ml-2';
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
li.append(infoDiv, dragHandle);
fileList.appendChild(li);
}
Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
});
hideLoader();
if (toolOptions && pageState.files.length >= 2) {
toolOptions.classList.remove('hidden');
}
} catch (error) {
console.error('Error loading PDFs:', error);
hideLoader();
showAlert('Error', 'Failed to load one or more PDF files.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function mixPages() {
if (pageState.pdfBytes.size < 2) {
showAlert('Not Enough Files', 'Please upload at least two PDF files to alternate and mix.');
return;
}
showLoader('Alternating and mixing pages...');
try {
const fileList = document.getElementById('file-list');
if (!fileList) throw new Error('File list not found');
const sortedFileNames = Array.from(fileList.children).map(function (li) {
return (li as HTMLElement).dataset.fileName;
}).filter(Boolean) as string[];
interface InterleaveFile {
name: string;
data: ArrayBuffer;
}
const filesToMerge: InterleaveFile[] = [];
for (const name of sortedFileNames) {
const bytes = pageState.pdfBytes.get(name);
if (bytes) {
filesToMerge.push({ name, data: bytes });
}
}
if (filesToMerge.length < 2) {
showAlert('Error', 'At least two valid PDFs are required.');
hideLoader();
return;
}
const message = {
command: 'interleave',
files: filesToMerge
};
alternateMergeWorker.postMessage(message, filesToMerge.map(function (f) { return f.data; }));
alternateMergeWorker.onmessage = function (e: MessageEvent) {
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!', 'success', function () {
resetState();
});
} else {
console.error('Worker interleave error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
}
};
alternateMergeWorker.onerror = function (e) {
hideLoader();
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) {
console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.');
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
pageState.files = pdfFiles;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', mixPages);
}
});

View File

@@ -1,150 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import Sortable from 'sortablejs';
interface AlternateMergeState {
pdfDocs: Record<string, any>;
pdfBytes: Record<string, ArrayBuffer>;
}
const alternateMergeState: AlternateMergeState = {
pdfDocs: {},
pdfBytes: {},
};
const alternateMergeWorker = new Worker(import.meta.env.BASE_URL + 'workers/alternate-merge.worker.js');
export async function setupAlternateMergeTool() {
const optionsDiv = document.getElementById('alternate-merge-options');
const processBtn = document.getElementById(
'process-btn'
) as HTMLButtonElement;
const fileList = document.getElementById('alternate-file-list');
if (!optionsDiv || !processBtn || !fileList) return;
optionsDiv.classList.remove('hidden');
processBtn.disabled = false;
processBtn.onclick = alternateMerge;
fileList.innerHTML = '';
alternateMergeState.pdfDocs = {};
alternateMergeState.pdfBytes = {};
showLoader('Loading PDF documents...');
try {
for (const file of state.files) {
const pdfBytes = await readFileAsArrayBuffer(file);
alternateMergeState.pdfBytes[file.name] = pdfBytes as ArrayBuffer;
const bytesForPdfJs = (pdfBytes as ArrayBuffer).slice(0);
const pdfjsDoc = await getPDFDocument({ data: bytesForPdfJs }).promise;
alternateMergeState.pdfDocs[file.name] = pdfjsDoc;
const pageCount = pdfjsDoc.numPages;
const li = document.createElement('li');
li.className =
'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
li.dataset.fileName = file.name;
const infoDiv = document.createElement('div');
infoDiv.className = 'flex items-center gap-2 truncate';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white';
nameSpan.textContent = file.name;
const pagesSpan = document.createElement('span');
pagesSpan.className = 'text-sm text-gray-400 flex-shrink-0';
pagesSpan.textContent = `(${pageCount} pages)`;
infoDiv.append(nameSpan, pagesSpan);
const dragHandle = document.createElement('div');
dragHandle.className =
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded';
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
li.append(infoDiv, dragHandle);
fileList.appendChild(li);
}
Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
});
} catch (error) {
showAlert(
'Error',
'Failed to load one or more PDF files. They may be corrupted or password-protected.'
);
console.error(error);
} finally {
hideLoader();
}
}
export async function alternateMerge() {
if (Object.keys(alternateMergeState.pdfBytes).length < 2) {
showAlert(
'Not Enough Files',
'Please upload at least two PDF files to alternate and mix.'
);
return;
}
showLoader('Alternating and mixing pages...');
try {
const fileList = document.getElementById('alternate-file-list');
if (!fileList) throw new Error('File list not found');
const sortedFileNames = Array.from(fileList.children).map(
(li) => (li as HTMLElement).dataset.fileName
).filter(Boolean) as string[];
const filesToMerge: InterleaveFile[] = [];
for (const name of sortedFileNames) {
const bytes = alternateMergeState.pdfBytes[name];
if (bytes) {
filesToMerge.push({ name, data: bytes });
}
}
if (filesToMerge.length < 2) {
showAlert('Error', 'At least two valid PDFs are required.');
hideLoader();
return;
}
const message: InterleaveMessage = {
command: 'interleave',
files: filesToMerge
};
alternateMergeWorker.postMessage(message, filesToMerge.map(f => f.data));
alternateMergeWorker.onmessage = (e: MessageEvent<InterleaveResponse>) => {
hideLoader();
if (e.data.status === 'success') {
const blob = new Blob([e.data.pdfBytes], { type: 'application/pdf' });
downloadFile(blob, 'alternated-mixed.pdf');
showAlert('Success', 'PDFs have been mixed successfully!');
} else {
console.error('Worker interleave error:', e.data.message);
showAlert('Error', e.data.message || 'Failed to interleave PDFs.');
}
};
alternateMergeWorker.onerror = (e) => {
hideLoader();
console.error('Worker error:', e);
showAlert('Error', 'An unexpected error occurred in the merge worker.');
};
} catch (e) {
console.error('Alternate Merge error:', e);
showAlert('Error', 'An error occurred while mixing the PDFs.');
hideLoader();
}
}

View File

@@ -0,0 +1,104 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
interface PageState { file: File | null; pdfDoc: PDFLibDocument | null; }
const pageState: PageState = { file: null, pdfDoc: null };
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); }
else { initializePage(); }
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; });
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault(); dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
});
}
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
if (processBtn) processBtn.addEventListener('click', changeBackgroundColor);
}
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); }
async function handleFiles(files: FileList) {
const file = files[0];
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
showLoader('Loading PDF...');
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
pageState.file = file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
finally { hideLoader(); }
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.pdfDoc.getPageCount()} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function resetState() {
pageState.file = null; pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function changeBackgroundColor() {
if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; }
const colorHex = (document.getElementById('background-color') as HTMLInputElement).value;
const color = hexToRgb(colorHex);
showLoader('Changing background color...');
try {
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < pageState.pdfDoc.getPageCount(); i++) {
const [originalPage] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
const { width, height } = originalPage.getSize();
const newPage = newPdfDoc.addPage([width, height]);
newPage.drawRectangle({ x: 0, y: 0, width, height, color: rgb(color.r, color.g, color.b) });
const embeddedPage = await newPdfDoc.embedPage(originalPage);
newPage.drawPage(embeddedPage, { x: 0, y: 0, width, height });
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
showAlert('Success', 'Background color changed successfully!', 'success', () => { resetState(); });
} catch (e) { console.error(e); showAlert('Error', 'Could not change the background color.'); }
finally { hideLoader(); }
}

View File

@@ -0,0 +1,204 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
let files: File[] = [];
async function convertImageToPngBytes(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Failed to get canvas context'));
return;
}
ctx.drawImage(img, 0, 0);
const pngBlob = await new Promise<Blob | null>((res) =>
canvas.toBlob(res, 'image/png')
);
if (!pngBlob) {
reject(new Error('Failed to create PNG blob'));
return;
}
const pngBytes = await pngBlob.arrayBuffer();
resolve(pngBytes);
};
img.onerror = () => reject(new Error('Failed to load image.'));
img.src = e.target?.result as string;
};
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsDataURL(file);
});
}
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const processBtn = document.getElementById('process-btn');
if (!fileDisplayArea || !fileControls || !processBtn) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
files.forEach((file, index) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-gray-200';
nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
infoContainer.append(nameSpan, sizeSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
}
};
const resetState = () => {
files = [];
updateUI();
};
async function convert() {
if (files.length === 0) {
showAlert('No Files', 'Please select at least one BMP file.');
return;
}
showLoader('Converting BMP to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of files) {
const pngBytes = await convertImageToPngBytes(file);
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_bmps.pdf'
);
showAlert('Success', 'PDF created successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert BMP to PDF. One of the files may be invalid.'
);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'image/bmp' || file.name.toLowerCase().endsWith('.bmp')
);
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped. Only BMP files are allowed.');
}
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
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');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput?.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -1,68 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
async function convertImageToPngBytes(file: any) {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const pngBlob = await new Promise((res) =>
canvas.toBlob(res, 'image/png')
);
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
resolve(pngBytes);
};
img.onerror = () => reject(new Error('Failed to load image.'));
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsDataURL(file);
});
}
export async function bmpToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one BMP file.');
return;
}
showLoader('Converting BMP to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await convertImageToPngBytes(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_bmps.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert BMP to PDF. One of the files may be invalid.'
);
} finally {
hideLoader();
}
}

View File

@@ -1,55 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
export async function changeBackgroundColor() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('background-color').value;
const color = hexToRgb(colorHex);
showLoader('Changing background color...');
try {
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
const { width, height } = originalPage.getSize();
const newPage = newPdfDoc.addPage([width, height]);
newPage.drawRectangle({
x: 0,
y: 0,
width,
height,
color: rgb(color.r, color.g, color.b),
});
const embeddedPage = await newPdfDoc.embedPage(originalPage);
newPage.drawPage(embeddedPage, {
x: 0,
y: 0,
width,
height,
});
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'background-changed.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change the background color.');
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,286 @@
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
interface PageState {
file: File | null;
}
const pageState: PageState = {
file: null,
};
function resetState() {
pageState.file = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const currentPassword = document.getElementById('current-password') as HTMLInputElement;
if (currentPassword) currentPassword.value = '';
const newUserPassword = document.getElementById('new-user-password') as HTMLInputElement;
if (newUserPassword) newUserPassword.value = '';
const newOwnerPassword = document.getElementById('new-owner-password') as HTMLInputElement;
if (newOwnerPassword) newOwnerPassword.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
async function changePermissions() {
if (!pageState.file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
const currentPassword = (document.getElementById('current-password') as HTMLInputElement)?.value || '';
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement)?.value || '';
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement)?.value || '';
const inputPath = '/input.pdf';
const outputPath = '/output.pdf';
let qpdf: any;
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
try {
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Initializing...';
qpdf = await initializeQpdf();
if (loaderText) loaderText.textContent = 'Reading PDF...';
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
if (loaderText) loaderText.textContent = 'Processing PDF permissions...';
const args = [inputPath];
if (currentPassword) {
args.push('--password=' + currentPassword);
}
const shouldEncrypt = newUserPassword || newOwnerPassword;
if (shouldEncrypt) {
const finalUserPassword = newUserPassword;
const finalOwnerPassword = newOwnerPassword;
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement)?.checked;
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement)?.checked;
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement)?.checked;
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement)?.checked;
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement)?.checked;
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement)?.checked;
const allowPageExtraction = (document.getElementById('allow-page-extraction') as HTMLInputElement)?.checked;
if (finalOwnerPassword) {
if (!allowModifying) args.push('--modify=none');
if (!allowCopying) args.push('--extract=n');
if (!allowPrinting) args.push('--print=none');
if (!allowAnnotating) args.push('--annotate=n');
if (!allowDocumentAssembly) args.push('--assemble=n');
if (!allowFillingForms) args.push('--form=n');
if (!allowPageExtraction) args.push('--extract=n');
if (!allowModifying) args.push('--modify-other=n');
} else if (finalUserPassword) {
args.push('--allow-insecure');
}
} else {
args.push('--decrypt');
}
args.push('--', outputPath);
try {
qpdf.callMain(args);
} catch (qpdfError: any) {
console.error('qpdf execution error:', qpdfError);
const errorMsg = qpdfError.message || '';
if (
errorMsg.includes('invalid password') ||
errorMsg.includes('incorrect password') ||
errorMsg.includes('password')
) {
throw new Error('INVALID_PASSWORD');
}
if (
errorMsg.includes('encrypted') ||
errorMsg.includes('password required')
) {
throw new Error('PASSWORD_REQUIRED');
}
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
}
if (loaderText) loaderText.textContent = 'Preparing download...';
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (!outputFile || outputFile.length === 0) {
throw new Error('Processing resulted in an empty file.');
}
const blob = new Blob([outputFile], { type: 'application/pdf' });
downloadFile(blob, `permissions-changed-${pageState.file.name}`);
if (loaderModal) loaderModal.classList.add('hidden');
let successMessage = 'PDF permissions changed successfully!';
if (!shouldEncrypt) {
successMessage = 'PDF decrypted successfully! All encryption and restrictions removed.';
}
showAlert('Success', successMessage, 'success', () => { resetState(); });
} catch (error: any) {
console.error('Error during PDF permission change:', error);
if (loaderModal) loaderModal.classList.add('hidden');
if (error.message === 'INVALID_PASSWORD') {
showAlert(
'Incorrect Password',
'The current password you entered is incorrect. Please try again.'
);
} else if (error.message === 'PASSWORD_REQUIRED') {
showAlert(
'Password Required',
'This PDF is password-protected. Please enter the current password to proceed.'
);
} else {
showAlert(
'Processing Failed',
`An error occurred: ${error.message || 'The PDF might be corrupted or password protected.'}`
);
}
} finally {
try {
if (qpdf?.FS) {
try {
qpdf.FS.unlink(inputPath);
} catch (e) { }
try {
qpdf.FS.unlink(outputPath);
} catch (e) { }
}
} catch (cleanupError) {
console.warn('Failed to cleanup WASM FS:', cleanupError);
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', changePermissions);
}
});

View File

@@ -1,169 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
initializeQpdf,
readFileAsArrayBuffer,
} from '../utils/helpers.js';
import { state } from '../state.js';
export async function changePermissions() {
const file = state.files[0];
const currentPassword =
(document.getElementById('current-password') as HTMLInputElement)?.value ||
'';
const newUserPassword =
(document.getElementById('new-user-password') as HTMLInputElement)?.value ||
'';
const newOwnerPassword =
(document.getElementById('new-owner-password') as HTMLInputElement)
?.value || '';
const inputPath = '/input.pdf';
const outputPath = '/output.pdf';
let qpdf: any;
try {
showLoader('Initializing...');
qpdf = await initializeQpdf();
showLoader('Reading PDF...');
const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
showLoader('Processing PDF permissions...');
const args = [inputPath];
// Add password if provided
if (currentPassword) {
args.push('--password=' + currentPassword);
}
const shouldEncrypt = newUserPassword || newOwnerPassword;
if (shouldEncrypt) {
const finalUserPassword = newUserPassword;
const finalOwnerPassword = newOwnerPassword;
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
const allowPrinting = (
document.getElementById('allow-printing') as HTMLInputElement
)?.checked;
const allowCopying = (
document.getElementById('allow-copying') as HTMLInputElement
)?.checked;
const allowModifying = (
document.getElementById('allow-modifying') as HTMLInputElement
)?.checked;
const allowAnnotating = (
document.getElementById('allow-annotating') as HTMLInputElement
)?.checked;
const allowFillingForms = (
document.getElementById('allow-filling-forms') as HTMLInputElement
)?.checked;
const allowDocumentAssembly = (
document.getElementById('allow-document-assembly') as HTMLInputElement
)?.checked;
const allowPageExtraction = (
document.getElementById('allow-page-extraction') as HTMLInputElement
)?.checked;
if (finalOwnerPassword) {
if (!allowModifying) args.push('--modify=none');
if (!allowCopying) args.push('--extract=n');
if (!allowPrinting) args.push('--print=none');
if (!allowAnnotating) args.push('--annotate=n');
if (!allowDocumentAssembly) args.push('--assemble=n');
if (!allowFillingForms) args.push('--form=n');
if (!allowPageExtraction) args.push('--extract=n');
// --modify-other is not directly mapped, apply if modifying is disabled
if (!allowModifying) args.push('--modify-other=n');
} else if (finalUserPassword) {
args.push('--allow-insecure');
}
} else {
args.push('--decrypt');
}
args.push('--', outputPath);
try {
qpdf.callMain(args);
} catch (qpdfError: any) {
console.error('qpdf execution error:', qpdfError);
const errorMsg = qpdfError.message || '';
if (
errorMsg.includes('invalid password') ||
errorMsg.includes('incorrect password') ||
errorMsg.includes('password')
) {
throw new Error('INVALID_PASSWORD');
}
if (
errorMsg.includes('encrypted') ||
errorMsg.includes('password required')
) {
throw new Error('PASSWORD_REQUIRED');
}
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
}
showLoader('Preparing download...');
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (!outputFile || outputFile.length === 0) {
throw new Error('Processing resulted in an empty file.');
}
const blob = new Blob([outputFile], { type: 'application/pdf' });
downloadFile(blob, `permissions-changed-${file.name}`);
hideLoader();
let successMessage = 'PDF permissions changed successfully!';
if (!shouldEncrypt) {
successMessage =
'PDF decrypted successfully! All encryption and restrictions removed.';
}
showAlert('Success', successMessage);
} catch (error: any) {
console.error('Error during PDF permission change:', error);
hideLoader();
if (error.message === 'INVALID_PASSWORD') {
showAlert(
'Incorrect Password',
'The current password you entered is incorrect. Please try again.'
);
} else if (error.message === 'PASSWORD_REQUIRED') {
showAlert(
'Password Required',
'This PDF is password-protected. Please enter the current password to proceed.'
);
} else {
showAlert(
'Processing Failed',
`An error occurred: ${error.message || 'The PDF might be corrupted or password protected.'}`
);
}
} finally {
try {
if (qpdf?.FS) {
try {
qpdf.FS.unlink(inputPath);
} catch (e) {}
try {
qpdf.FS.unlink(outputPath);
} catch (e) {}
}
} catch (cleanupError) {
console.warn('Failed to cleanup WASM FS:', cleanupError);
}
}
}

View File

@@ -1,172 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
hexToRgb,
readFileAsArrayBuffer,
getPDFDocument,
} from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let isRenderingPreview = false;
let renderTimeout: any;
async function updateTextColorPreview() {
if (isRenderingPreview) return;
isRenderingPreview = true;
try {
const textColorCanvas = document.getElementById('text-color-canvas') as HTMLCanvasElement;
if (!textColorCanvas) return;
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const page = await pdf.getPage(1); // Preview first page
const viewport = page.getViewport({ scale: 0.8 });
const context = textColorCanvas.getContext('2d');
textColorCanvas.width = viewport.width;
textColorCanvas.height = viewport.height;
await page.render({ canvasContext: context, viewport, canvas: textColorCanvas }).promise;
const imageData = context.getImageData(
0,
0,
textColorCanvas.width,
textColorCanvas.height
);
const data = imageData.data;
const colorHex = (
document.getElementById('text-color-input') as HTMLInputElement
).value;
const { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120;
for (let i = 0; i < data.length; i += 4) {
if (
data[i] < darknessThreshold &&
data[i + 1] < darknessThreshold &&
data[i + 2] < darknessThreshold
) {
data[i] = r * 255;
data[i + 1] = g * 255;
data[i + 2] = b * 255;
}
}
context.putImageData(imageData, 0, 0);
} catch (error) {
console.error('Error updating preview:', error);
} finally {
isRenderingPreview = false;
}
}
export async function setupTextColorTool() {
const originalCanvas = document.getElementById('original-canvas');
const colorInput = document.getElementById('text-color-input');
if (!originalCanvas || !colorInput) return;
// Debounce the preview update for performance
colorInput.addEventListener('input', () => {
clearTimeout(renderTimeout);
renderTimeout = setTimeout(updateTextColorPreview, 250);
});
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 0.8 });
(originalCanvas as HTMLCanvasElement).width = viewport.width;
(originalCanvas as HTMLCanvasElement).height = viewport.height;
await page.render({
canvasContext: (originalCanvas as HTMLCanvasElement).getContext('2d'),
viewport,
canvas: originalCanvas as HTMLCanvasElement,
}).promise;
await updateTextColorPreview();
}
export async function changeTextColor() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
const colorHex = (document.getElementById('text-color-input') as HTMLInputElement).value;
const { r, g, b } = hexToRgb(colorHex);
const darknessThreshold = 120;
showLoader('Changing text color...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 }); // High resolution for quality
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport, canvas }).promise;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
if (
data[j] < darknessThreshold &&
data[j + 1] < darknessThreshold &&
data[j + 2] < darknessThreshold
) {
data[j] = r * 255;
data[j + 1] = g * 255;
data[j + 2] = b * 255;
}
}
context.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
reader.readAsArrayBuffer(blob!);
}, 'image/png')
);
const pngImage = await newPdfDoc.embedPng(pngImageBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(pngImage, {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
});
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'text-color-changed.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change text color.');
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,313 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface CombineState {
file: File | null;
pdfDoc: PDFLibDocument | null;
}
const pageState: CombineState = {
file: null,
pdfDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function combineToSinglePage() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
const orientation = (document.getElementById('combine-orientation') as HTMLSelectElement).value;
const spacing = parseInt((document.getElementById('page-spacing') as HTMLInputElement).value) || 0;
const backgroundColorHex = (document.getElementById('background-color') as HTMLInputElement).value;
const addSeparator = (document.getElementById('add-separator') as HTMLInputElement).checked;
const separatorThickness = parseFloat((document.getElementById('separator-thickness') as HTMLInputElement).value) || 0.5;
const separatorColorHex = (document.getElementById('separator-color') as HTMLInputElement).value;
const backgroundColor = hexToRgb(backgroundColorHex);
const separatorColor = hexToRgb(separatorColorHex);
showLoader('Combining pages...');
try {
const sourceDoc = pageState.pdfDoc;
const newDoc = await PDFLibDocument.create();
const pdfBytes = await sourceDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
const sourcePages = sourceDoc.getPages();
let maxWidth = 0;
let maxHeight = 0;
let totalWidth = 0;
let totalHeight = 0;
sourcePages.forEach(function (page) {
const { width, height } = page.getSize();
if (width > maxWidth) maxWidth = width;
if (height > maxHeight) maxHeight = height;
totalWidth += width;
totalHeight += height;
});
let finalWidth: number, finalHeight: number;
if (orientation === 'horizontal') {
finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing;
finalHeight = maxHeight;
} else {
finalWidth = maxWidth;
finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing;
}
const newPage = newDoc.addPage([finalWidth, finalHeight]);
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
newPage.drawRectangle({
x: 0,
y: 0,
width: finalWidth,
height: finalHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
}
let currentX = 0;
let currentY = finalHeight;
for (let i = 0; i < sourcePages.length; i++) {
showLoader(`Processing page ${i + 1} of ${sourcePages.length}...`);
const sourcePage = sourcePages[i];
const { width, height } = sourcePage.getSize();
try {
const page = await pdfjsDoc.getPage(i + 1);
const scale = 2.0;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
viewport,
canvas
}).promise;
const pngDataUrl = canvas.toDataURL('image/png');
const pngImage = await newDoc.embedPng(pngDataUrl);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
newPage.drawImage(pngImage, { x: currentX, y, width, height });
} else {
currentY -= height;
const x = (finalWidth - width) / 2;
newPage.drawImage(pngImage, { x, y: currentY, width, height });
}
} catch (renderError) {
console.warn(`Failed to render page ${i + 1}:`, renderError);
}
if (addSeparator && i < sourcePages.length - 1) {
if (orientation === 'horizontal') {
const lineX = currentX + width + spacing / 2;
newPage.drawLine({
start: { x: lineX, y: 0 },
end: { x: lineX, y: finalHeight },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentX += width + spacing;
} else {
const lineY = currentY - spacing / 2;
newPage.drawLine({
start: { x: 0, y: lineY },
end: { x: finalWidth, y: lineY },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentY -= spacing;
}
} else {
if (orientation === 'horizontal') {
currentX += width + spacing;
} else {
currentY -= spacing;
}
}
}
const newPdfBytes = await newDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_combined.pdf`
);
showAlert('Success', 'Pages combined successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while combining pages.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addSeparatorCheckbox = document.getElementById('add-separator');
const separatorOptions = document.getElementById('separator-options');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (addSeparatorCheckbox && separatorOptions) {
addSeparatorCheckbox.addEventListener('change', function () {
if ((addSeparatorCheckbox as HTMLInputElement).checked) {
separatorOptions.classList.remove('hidden');
} else {
separatorOptions.classList.add('hidden');
}
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', combineToSinglePage);
}
});

View File

@@ -1,219 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
document.addEventListener('change', (e) => {
const target = e.target as HTMLElement;
if (target.id === 'add-separator') {
const separatorOptions = document.getElementById('separator-options');
if (separatorOptions) {
const checkbox = target as HTMLInputElement;
if (checkbox.checked) {
separatorOptions.classList.remove('hidden');
} else {
separatorOptions.classList.add('hidden');
}
}
}
});
export async function combineToSinglePage() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const orientation = document.getElementById('combine-orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const backgroundColorHex = document.getElementById('background-color').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addSeparator = document.getElementById('add-separator').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const separatorThickness = parseFloat(document.getElementById('separator-thickness').value) || 0.5;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const separatorColorHex = document.getElementById('separator-color').value;
const backgroundColor = hexToRgb(backgroundColorHex);
const separatorColor = hexToRgb(separatorColorHex);
showLoader('Combining pages...');
try {
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
const pdfBytes = await sourceDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
const sourcePages = sourceDoc.getPages();
let maxWidth = 0;
let maxHeight = 0;
let totalWidth = 0;
let totalHeight = 0;
sourcePages.forEach((page: any) => {
const { width, height } = page.getSize();
if (width > maxWidth) maxWidth = width;
if (height > maxHeight) maxHeight = height;
totalWidth += width;
totalHeight += height;
});
let finalWidth, finalHeight;
if (orientation === 'horizontal') {
finalWidth = totalWidth + Math.max(0, sourcePages.length - 1) * spacing;
finalHeight = maxHeight;
} else {
finalWidth = maxWidth;
finalHeight = totalHeight + Math.max(0, sourcePages.length - 1) * spacing;
}
const newPage = newDoc.addPage([finalWidth, finalHeight]);
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
newPage.drawRectangle({
x: 0,
y: 0,
width: finalWidth,
height: finalHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
}
let currentX = 0;
let currentY = finalHeight;
for (let i = 0; i < sourcePages.length; i++) {
const sourcePage = sourcePages[i];
const { width, height } = sourcePage.getSize();
try {
const page = await pdfjsDoc.getPage(i + 1);
const scale = 2.0;
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d')!;
await page.render({
canvasContext: context,
viewport,
canvas
}).promise;
const pngDataUrl = canvas.toDataURL('image/png');
const pngImage = await newDoc.embedPng(pngDataUrl);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
newPage.drawImage(pngImage, { x: currentX, y, width, height });
} else {
// Vertical layout: stack top to bottom
currentY -= height;
const x = (finalWidth - width) / 2; // Center horizontally
newPage.drawImage(pngImage, { x, y: currentY, width, height });
}
} catch (renderError) {
console.warn(`Failed to render page ${i + 1} with PDF.js, trying fallback method:`, renderError);
// Fallback: try to copy and embed the page directly
try {
const [copiedPage] = await newDoc.copyPages(sourceDoc, [i]);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
const embeddedPage = await newDoc.embedPage(copiedPage);
newPage.drawPage(embeddedPage, { x: currentX, y, width, height });
} else {
currentY -= height;
const x = (finalWidth - width) / 2;
const embeddedPage = await newDoc.embedPage(copiedPage);
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
}
} catch (embedError) {
console.error(`Failed to process page ${i + 1}:`, embedError);
if (orientation === 'horizontal') {
const y = (finalHeight - height) / 2;
newPage.drawRectangle({
x: currentX,
y,
width,
height,
borderColor: rgb(0.8, 0, 0),
borderWidth: 2,
});
newPage.drawText(`Page ${i + 1} could not be rendered`, {
x: currentX + 10,
y: y + height / 2,
size: 12,
color: rgb(0.8, 0, 0),
});
} else {
currentY -= height;
const x = (finalWidth - width) / 2;
newPage.drawRectangle({
x,
y: currentY,
width,
height,
borderColor: rgb(0.8, 0, 0),
borderWidth: 2,
});
newPage.drawText(`Page ${i + 1} could not be rendered`, {
x: x + 10,
y: currentY + height / 2,
size: 12,
color: rgb(0.8, 0, 0),
});
}
}
}
// Draw separator line
if (addSeparator && i < sourcePages.length - 1) {
if (orientation === 'horizontal') {
const lineX = currentX + width + spacing / 2;
newPage.drawLine({
start: { x: lineX, y: 0 },
end: { x: lineX, y: finalHeight },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentX += width + spacing;
} else {
const lineY = currentY - spacing / 2;
newPage.drawLine({
start: { x: 0, y: lineY },
end: { x: finalWidth, y: lineY },
thickness: separatorThickness,
color: rgb(separatorColor.r, separatorColor.g, separatorColor.b),
});
currentY -= spacing;
}
} else {
if (orientation === 'horizontal') {
currentX += width + spacing;
} else {
currentY -= spacing;
}
}
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'combined-page.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while combining pages.');
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,306 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { getPDFDocument } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface CompareState {
pdfDoc1: pdfjsLib.PDFDocumentProxy | null;
pdfDoc2: pdfjsLib.PDFDocumentProxy | null;
currentPage: number;
viewMode: 'overlay' | 'side-by-side';
isSyncScroll: boolean;
}
const pageState: CompareState = {
pdfDoc1: null,
pdfDoc2: null,
currentPage: 1,
viewMode: 'overlay',
isSyncScroll: true,
};
async function renderPage(
pdfDoc: pdfjsLib.PDFDocumentProxy,
pageNum: number,
canvas: HTMLCanvasElement,
container: HTMLElement
) {
const page = await pdfDoc.getPage(pageNum);
const containerWidth = container.clientWidth - 2;
const viewport = page.getViewport({ scale: 1.0 });
const scale = containerWidth / viewport.width;
const scaledViewport = page.getViewport({ scale: scale });
canvas.width = scaledViewport.width;
canvas.height = scaledViewport.height;
await page.render({
canvasContext: canvas.getContext('2d')!,
viewport: scaledViewport,
canvas
}).promise;
}
async function renderBothPages() {
if (!pageState.pdfDoc1 || !pageState.pdfDoc2) return;
showLoader(`Loading page ${pageState.currentPage}...`);
const canvas1 = document.getElementById('canvas-compare-1') as HTMLCanvasElement;
const canvas2 = document.getElementById('canvas-compare-2') as HTMLCanvasElement;
const panel1 = document.getElementById('panel-1') as HTMLElement;
const panel2 = document.getElementById('panel-2') as HTMLElement;
const wrapper = document.getElementById('compare-viewer-wrapper') as HTMLElement;
const container1 = pageState.viewMode === 'overlay' ? wrapper : panel1;
const container2 = pageState.viewMode === 'overlay' ? wrapper : panel2;
await Promise.all([
renderPage(
pageState.pdfDoc1,
Math.min(pageState.currentPage, pageState.pdfDoc1.numPages),
canvas1,
container1
),
renderPage(
pageState.pdfDoc2,
Math.min(pageState.currentPage, pageState.pdfDoc2.numPages),
canvas2,
container2
),
]);
updateNavControls();
hideLoader();
}
function updateNavControls() {
const maxPages = Math.max(
pageState.pdfDoc1?.numPages || 0,
pageState.pdfDoc2?.numPages || 0
);
const currentDisplay = document.getElementById('current-page-display-compare');
const totalDisplay = document.getElementById('total-pages-display-compare');
const prevBtn = document.getElementById('prev-page-compare') as HTMLButtonElement;
const nextBtn = document.getElementById('next-page-compare') as HTMLButtonElement;
if (currentDisplay) currentDisplay.textContent = pageState.currentPage.toString();
if (totalDisplay) totalDisplay.textContent = maxPages.toString();
if (prevBtn) prevBtn.disabled = pageState.currentPage <= 1;
if (nextBtn) nextBtn.disabled = pageState.currentPage >= maxPages;
}
function setViewMode(mode: 'overlay' | 'side-by-side') {
pageState.viewMode = mode;
const wrapper = document.getElementById('compare-viewer-wrapper');
const overlayControls = document.getElementById('overlay-controls');
const sideControls = document.getElementById('side-by-side-controls');
const btnOverlay = document.getElementById('view-mode-overlay');
const btnSide = document.getElementById('view-mode-side');
const canvas2 = document.getElementById('canvas-compare-2') as HTMLCanvasElement;
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
if (mode === 'overlay') {
if (wrapper) wrapper.className = 'compare-viewer-wrapper overlay-mode bg-gray-900 rounded-lg border border-gray-700 min-h-[400px] relative';
if (overlayControls) overlayControls.classList.remove('hidden');
if (sideControls) sideControls.classList.add('hidden');
if (btnOverlay) {
btnOverlay.classList.add('bg-indigo-600');
btnOverlay.classList.remove('bg-gray-700');
}
if (btnSide) {
btnSide.classList.remove('bg-indigo-600');
btnSide.classList.add('bg-gray-700');
}
if (canvas2 && opacitySlider) canvas2.style.opacity = opacitySlider.value;
} else {
if (wrapper) wrapper.className = 'compare-viewer-wrapper side-by-side-mode bg-gray-900 rounded-lg border border-gray-700 min-h-[400px]';
if (overlayControls) overlayControls.classList.add('hidden');
if (sideControls) sideControls.classList.remove('hidden');
if (btnOverlay) {
btnOverlay.classList.remove('bg-indigo-600');
btnOverlay.classList.add('bg-gray-700');
}
if (btnSide) {
btnSide.classList.add('bg-indigo-600');
btnSide.classList.remove('bg-gray-700');
}
if (canvas2) canvas2.style.opacity = '1';
}
renderBothPages();
}
async function handleFileInput(inputId: string, docKey: 'pdfDoc1' | 'pdfDoc2', displayId: string) {
const fileInput = document.getElementById(inputId) as HTMLInputElement;
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
async function handleFile(file: File) {
if (!file || file.type !== 'application/pdf') {
showAlert('Invalid File', 'Please select a valid PDF file.');
return;
}
const displayDiv = document.getElementById(displayId);
if (displayDiv) {
displayDiv.innerHTML = '';
const icon = document.createElement('i');
icon.setAttribute('data-lucide', 'check-circle');
icon.className = 'w-10 h-10 mb-3 text-green-500';
const p = document.createElement('p');
p.className = 'text-sm text-gray-300 truncate';
p.textContent = file.name;
displayDiv.append(icon, p);
createIcons({ icons });
}
try {
showLoader(`Loading ${file.name}...`);
const arrayBuffer = await file.arrayBuffer();
pageState[docKey] = await getPDFDocument({ data: arrayBuffer }).promise;
if (pageState.pdfDoc1 && pageState.pdfDoc2) {
const compareViewer = document.getElementById('compare-viewer');
if (compareViewer) compareViewer.classList.remove('hidden');
pageState.currentPage = 1;
await renderBothPages();
}
} catch (e) {
showAlert('Error', 'Could not load PDF. It may be corrupt or password-protected.');
console.error(e);
} finally {
hideLoader();
}
}
if (fileInput) {
fileInput.addEventListener('change', function (e) {
const files = (e.target as HTMLInputElement).files;
if (files && files[0]) handleFile(files[0]);
});
}
if (dropZone) {
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
const files = e.dataTransfer?.files;
if (files && files[0]) handleFile(files[0]);
});
}
}
document.addEventListener('DOMContentLoaded', function () {
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
handleFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
handleFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
const prevBtn = document.getElementById('prev-page-compare');
const nextBtn = document.getElementById('next-page-compare');
if (prevBtn) {
prevBtn.addEventListener('click', function () {
if (pageState.currentPage > 1) {
pageState.currentPage--;
renderBothPages();
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () {
const maxPages = Math.max(
pageState.pdfDoc1?.numPages || 0,
pageState.pdfDoc2?.numPages || 0
);
if (pageState.currentPage < maxPages) {
pageState.currentPage++;
renderBothPages();
}
});
}
const btnOverlay = document.getElementById('view-mode-overlay');
const btnSide = document.getElementById('view-mode-side');
if (btnOverlay) {
btnOverlay.addEventListener('click', function () {
setViewMode('overlay');
});
}
if (btnSide) {
btnSide.addEventListener('click', function () {
setViewMode('side-by-side');
});
}
const flickerBtn = document.getElementById('flicker-btn');
const canvas2 = document.getElementById('canvas-compare-2') as HTMLCanvasElement;
const opacitySlider = document.getElementById('opacity-slider') as HTMLInputElement;
// Track flicker state
let flickerVisible = true;
if (flickerBtn && canvas2) {
flickerBtn.addEventListener('click', function () {
flickerVisible = !flickerVisible;
canvas2.style.transition = 'opacity 150ms ease-in-out';
canvas2.style.opacity = flickerVisible ? (opacitySlider?.value || '0.5') : '0';
});
}
if (opacitySlider && canvas2) {
opacitySlider.addEventListener('input', function () {
flickerVisible = true; // Reset flicker state when slider changes
canvas2.style.transition = '';
canvas2.style.opacity = opacitySlider.value;
});
}
const panel1 = document.getElementById('panel-1');
const panel2 = document.getElementById('panel-2');
const syncToggle = document.getElementById('sync-scroll-toggle') as HTMLInputElement;
if (syncToggle) {
syncToggle.addEventListener('change', function () {
pageState.isSyncScroll = syncToggle.checked;
});
}
let scrollingPanel: HTMLElement | null = null;
if (panel1 && panel2) {
panel1.addEventListener('scroll', function () {
if (pageState.isSyncScroll && scrollingPanel !== panel2) {
scrollingPanel = panel1;
panel2.scrollTop = panel1.scrollTop;
setTimeout(function () { scrollingPanel = null; }, 100);
}
});
panel2.addEventListener('scroll', function () {
if (pageState.isSyncScroll && scrollingPanel !== panel1) {
scrollingPanel = panel2;
panel1.scrollTop = panel2.scrollTop;
setTimeout(function () { scrollingPanel = null; }, 100);
}
});
}
createIcons({ icons });
});

View File

@@ -1,248 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
const state = {
pdfDoc1: null,
pdfDoc2: null,
currentPage: 1,
viewMode: 'overlay',
isSyncScroll: true,
};
/**
* Renders a PDF page to fit the width of its container.
* @param {PDFDocumentProxy} pdfDoc - The loaded PDF document from pdf.js.
* @param {number} pageNum - The page number to render.
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
* @param {HTMLElement} container - The container to fit the canvas into.
*/
async function renderPage(
pdfDoc: any,
pageNum: any,
canvas: any,
container: any
) {
const page = await pdfDoc.getPage(pageNum);
// Calculate scale to fit the container width.
const containerWidth = container.clientWidth - 2; // Subtract border width
const viewport = page.getViewport({ scale: 1.0 });
const scale = containerWidth / viewport.width;
const scaledViewport = page.getViewport({ scale: scale });
canvas.width = scaledViewport.width;
canvas.height = scaledViewport.height;
await page.render({
canvasContext: canvas.getContext('2d'),
viewport: scaledViewport,
}).promise;
}
async function renderBothPages() {
if (!state.pdfDoc1 || !state.pdfDoc2) return;
showLoader(`Loading page ${state.currentPage}...`);
const canvas1 = document.getElementById('canvas-compare-1');
const canvas2 = document.getElementById('canvas-compare-2');
const panel1 = document.getElementById('panel-1');
const panel2 = document.getElementById('panel-2');
const wrapper = document.getElementById('compare-viewer-wrapper');
// Determine the correct container based on the view mode
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
await Promise.all([
renderPage(
state.pdfDoc1,
Math.min(state.currentPage, state.pdfDoc1.numPages),
canvas1,
container1
),
renderPage(
state.pdfDoc2,
Math.min(state.currentPage, state.pdfDoc2.numPages),
canvas2,
container2
),
]);
updateNavControls();
hideLoader();
}
function updateNavControls() {
const maxPages = Math.max(
state.pdfDoc1?.numPages || 0,
state.pdfDoc2?.numPages || 0
);
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
document.getElementById('current-page-display-compare').textContent =
state.currentPage;
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
document.getElementById('total-pages-display-compare').textContent = maxPages;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('prev-page-compare').disabled =
state.currentPage <= 1;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('next-page-compare').disabled =
state.currentPage >= maxPages;
}
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
const fileInput = document.getElementById(inputId);
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
const handleFile = async (file: any) => {
if (!file || file.type !== 'application/pdf')
return showAlert('Invalid File', 'Please select a valid PDF file.');
const displayDiv = document.getElementById(displayId);
displayDiv.textContent = '';
// 2. Create the icon element
const icon = document.createElement('i');
icon.setAttribute('data-lucide', 'check-circle');
icon.className = 'w-10 h-10 mb-3 text-green-500';
// 3. Create the paragraph element for the file name
const p = document.createElement('p');
p.className = 'text-sm text-gray-300 truncate';
// 4. Set the file name safely using textContent
p.textContent = file.name;
// 5. Append the safe elements to the container
displayDiv.append(icon, p);
createIcons({ icons });
try {
showLoader(`Loading ${file.name}...`);
const pdfBytes = await readFileAsArrayBuffer(file);
state[docKey] = await getPDFDocument(pdfBytes).promise;
if (state.pdfDoc1 && state.pdfDoc2) {
document.getElementById('compare-viewer').classList.remove('hidden');
state.currentPage = 1;
await renderBothPages();
}
} catch (e) {
showAlert(
'Error',
'Could not load PDF. It may be corrupt or password-protected.'
);
console.error(e);
} finally {
hideLoader();
}
};
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
dropZone.addEventListener('dragover', (e) => e.preventDefault());
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
handleFile(e.dataTransfer.files[0]);
});
}
/**
* Toggles the UI between Overlay and Side-by-Side views.
* @param {'overlay' | 'side-by-side'} mode
*/
function setViewMode(mode: any) {
state.viewMode = mode;
const wrapper = document.getElementById('compare-viewer-wrapper');
const overlayControls = document.getElementById('overlay-controls');
const sideControls = document.getElementById('side-by-side-controls');
const btnOverlay = document.getElementById('view-mode-overlay');
const btnSide = document.getElementById('view-mode-side');
const canvas2 = document.getElementById('canvas-compare-2');
const opacitySlider = document.getElementById('opacity-slider');
if (mode === 'overlay') {
wrapper.className = 'compare-viewer-wrapper overlay-mode';
overlayControls.classList.remove('hidden');
sideControls.classList.add('hidden');
btnOverlay.classList.add('bg-indigo-600');
btnSide.classList.remove('bg-indigo-600');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
canvas2.style.opacity = opacitySlider.value;
} else {
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
overlayControls.classList.add('hidden');
sideControls.classList.remove('hidden');
btnOverlay.classList.remove('bg-indigo-600');
btnSide.classList.add('bg-indigo-600');
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
canvas2.style.opacity = '1';
}
renderBothPages();
}
export function setupCompareTool() {
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
document.getElementById('prev-page-compare').addEventListener('click', () => {
if (state.currentPage > 1) {
state.currentPage--;
renderBothPages();
}
});
document.getElementById('next-page-compare').addEventListener('click', () => {
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
if (state.currentPage < maxPages) {
state.currentPage++;
renderBothPages();
}
});
document
.getElementById('view-mode-overlay')
.addEventListener('click', () => setViewMode('overlay'));
document
.getElementById('view-mode-side')
.addEventListener('click', () => setViewMode('side-by-side'));
const canvas2 = document.getElementById('canvas-compare-2');
document.getElementById('flicker-btn').addEventListener('click', () => {
canvas2.style.transition = 'opacity 150ms ease-in-out';
canvas2.style.opacity = canvas2.style.opacity === '0' ? '1' : '0';
});
document.getElementById('opacity-slider').addEventListener('input', (e) => {
canvas2.style.transition = '';
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
canvas2.style.opacity = e.target.value;
});
const panel1 = document.getElementById('panel-1');
const panel2 = document.getElementById('panel-2');
const syncToggle = document.getElementById('sync-scroll-toggle');
(syncToggle as HTMLInputElement).addEventListener('change', () => {
state.isSyncScroll = (syncToggle as HTMLInputElement).checked;
});
let scrollingPanel: any = null;
panel1.addEventListener('scroll', () => {
if (state.isSyncScroll && scrollingPanel !== panel2) {
scrollingPanel = panel1;
panel2.scrollTop = panel1.scrollTop;
setTimeout(() => (scrollingPanel = null), 100);
}
});
panel2.addEventListener('scroll', () => {
if (state.isSyncScroll && scrollingPanel !== panel1) {
scrollingPanel = panel2;
panel1.scrollTop = panel2.scrollTop;
setTimeout(() => (scrollingPanel = null), 100);
}
});
}

View File

@@ -247,24 +247,15 @@ document.addEventListener('DOMContentLoaded', () => {
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSizeContainer = document.createElement('div');
nameSizeContainer.className = 'flex items-center gap-2';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-gray-200';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`;
nameSizeContainer.append(nameSpan, sizeSpan);
const pagesSpan = document.createElement('span');
pagesSpan.className = 'text-xs text-gray-500 mt-0.5';
pagesSpan.textContent = 'Loading pages...';
infoContainer.append(nameSizeContainer, pagesSpan);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
@@ -280,10 +271,10 @@ document.addEventListener('DOMContentLoaded', () => {
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await getPDFDocument({ data: arrayBuffer }).promise;
pagesSpan.textContent = `${pdfDoc.numPages} Pages`;
metaSpan.textContent = `${formatBytes(file.size)}${pdfDoc.numPages} pages`;
} catch (error) {
console.error('Error loading PDF:', error);
pagesSpan.textContent = 'Could not load page count';
metaSpan.textContent = `${formatBytes(file.size)}Could not load page count`;
}
}
@@ -512,7 +503,6 @@ document.addEventListener('DOMContentLoaded', () => {
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
fileInput.value = '';
});
dropZone.addEventListener('dragover', (e) => {
@@ -539,8 +529,9 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
dropZone.addEventListener('click', () => {
fileInput.click();
// Clear value on click to allow re-selecting the same file
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}

View File

@@ -0,0 +1,381 @@
import { createIcons, icons } from 'lucide';
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, formatBytes, getPDFDocument } from '../utils/helpers.js';
import Cropper from 'cropperjs';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface CropperState {
pdfDoc: any;
currentPageNum: number;
cropper: any;
originalPdfBytes: ArrayBuffer | null;
pageCrops: Record<number, any>;
file: File | null;
}
const cropperState: CropperState = {
pdfDoc: null,
currentPageNum: 1,
cropper: null,
originalPdfBytes: null,
pageCrops: {},
file: null,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
if (fileInput) fileInput.addEventListener('change', handleFileUpload);
if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
document.getElementById('prev-page')?.addEventListener('click', () => changePage(-1));
document.getElementById('next-page')?.addEventListener('click', () => changePage(1));
document.getElementById('crop-button')?.addEventListener('click', performCrop);
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) handleFile(input.files[0]);
}
async function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
showLoader('Loading PDF...');
cropperState.file = file;
cropperState.pageCrops = {};
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
cropperState.originalPdfBytes = arrayBuffer as ArrayBuffer;
cropperState.pdfDoc = await getPDFDocument({ data: (arrayBuffer as ArrayBuffer).slice(0) }).promise;
cropperState.currentPageNum = 1;
updateFileDisplay();
await displayPageAsImage(cropperState.currentPageNum);
hideLoader();
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
}
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !cropperState.file) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = cropperState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(cropperState.file.size)}${cropperState.pdfDoc?.numPages || 0} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => resetState();
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function saveCurrentCrop() {
if (cropperState.cropper) {
const currentCrop = cropperState.cropper.getData(true);
const imageData = cropperState.cropper.getImageData();
const cropPercentages = {
x: currentCrop.x / imageData.naturalWidth,
y: currentCrop.y / imageData.naturalHeight,
width: currentCrop.width / imageData.naturalWidth,
height: currentCrop.height / imageData.naturalHeight,
};
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
}
}
async function displayPageAsImage(num: number) {
showLoader(`Rendering Page ${num}...`);
try {
const page = await cropperState.pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: 2.5 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
if (cropperState.cropper) cropperState.cropper.destroy();
const cropperEditor = document.getElementById('cropper-editor');
if (cropperEditor) cropperEditor.classList.remove('hidden');
const container = document.getElementById('cropper-container');
if (!container) return;
container.innerHTML = '';
const image = document.createElement('img');
image.src = tempCanvas.toDataURL('image/png');
container.appendChild(image);
image.onload = () => {
cropperState.cropper = new Cropper(image, {
viewMode: 1,
background: false,
autoCropArea: 0.8,
responsive: true,
rotatable: false,
zoomable: false,
});
const savedCrop = cropperState.pageCrops[num];
if (savedCrop) {
const imageData = cropperState.cropper.getImageData();
cropperState.cropper.setData({
x: savedCrop.x * imageData.naturalWidth,
y: savedCrop.y * imageData.naturalHeight,
width: savedCrop.width * imageData.naturalWidth,
height: savedCrop.height * imageData.naturalHeight,
});
}
updatePageInfo();
enableControls();
hideLoader();
};
} catch (error) {
console.error('Error rendering page:', error);
showAlert('Error', 'Failed to render page.');
hideLoader();
}
}
async function changePage(offset: number) {
saveCurrentCrop();
const newPageNum = cropperState.currentPageNum + offset;
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
cropperState.currentPageNum = newPageNum;
await displayPageAsImage(cropperState.currentPageNum);
}
}
function updatePageInfo() {
const pageInfo = document.getElementById('page-info');
if (pageInfo) pageInfo.textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
}
function enableControls() {
const prevBtn = document.getElementById('prev-page') as HTMLButtonElement;
const nextBtn = document.getElementById('next-page') as HTMLButtonElement;
const cropBtn = document.getElementById('crop-button') as HTMLButtonElement;
if (prevBtn) prevBtn.disabled = cropperState.currentPageNum <= 1;
if (nextBtn) nextBtn.disabled = cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
if (cropBtn) cropBtn.disabled = false;
}
async function performCrop() {
saveCurrentCrop();
const isDestructive = (document.getElementById('destructive-crop-toggle') as HTMLInputElement)?.checked;
const isApplyToAll = (document.getElementById('apply-to-all-toggle') as HTMLInputElement)?.checked;
let finalCropData: Record<number, any> = {};
if (isApplyToAll) {
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
if (!currentCrop) {
showAlert('No Crop Area', 'Please select an area to crop first.');
return;
}
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
finalCropData[i] = currentCrop;
}
} else {
finalCropData = { ...cropperState.pageCrops };
}
if (Object.keys(finalCropData).length === 0) {
showAlert('No Crop Area', 'Please select an area on at least one page to crop.');
return;
}
showLoader('Applying crop...');
try {
let finalPdfBytes;
if (isDestructive) {
finalPdfBytes = await performFlatteningCrop(finalCropData);
} else {
finalPdfBytes = await performMetadataCrop(finalCropData);
}
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
downloadFile(new Blob([finalPdfBytes], { type: 'application/pdf' }), fileName);
showAlert('Success', 'Crop complete! Your download has started.', 'success', () => resetState());
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred during cropping.');
} finally {
hideLoader();
}
}
async function performMetadataCrop(cropData: Record<number, any>): Promise<Uint8Array> {
const pdfToModify = await PDFLibDocument.load(cropperState.originalPdfBytes!, { ignoreEncryption: true, throwOnInvalidObject: false });
for (const pageNum in cropData) {
const pdfJsPage = await cropperState.pdfDoc.getPage(Number(pageNum));
const viewport = pdfJsPage.getViewport({ scale: 1 });
const crop = cropData[pageNum];
const cropX = viewport.width * crop.x;
const cropY = viewport.height * crop.y;
const cropW = viewport.width * crop.width;
const cropH = viewport.height * crop.height;
const visualCorners = [
{ x: cropX, y: cropY },
{ x: cropX + cropW, y: cropY },
{ x: cropX + cropW, y: cropY + cropH },
{ x: cropX, y: cropY + cropH },
];
const pdfCorners = visualCorners.map(p => viewport.convertToPdfPoint(p.x, p.y));
const pdfXs = pdfCorners.map(p => p[0]);
const pdfYs = pdfCorners.map(p => p[1]);
const minX = Math.min(...pdfXs);
const maxX = Math.max(...pdfXs);
const minY = Math.min(...pdfYs);
const maxY = Math.max(...pdfYs);
const page = pdfToModify.getPages()[Number(pageNum) - 1];
page.setCropBox(minX, minY, maxX - minX, maxY - minY);
}
return pdfToModify.save();
}
async function performFlatteningCrop(cropData: Record<number, any>): Promise<Uint8Array> {
const newPdfDoc = await PDFLibDocument.create();
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes!, { ignoreEncryption: true, throwOnInvalidObject: false });
const totalPages = cropperState.pdfDoc.numPages;
for (let i = 0; i < totalPages; i++) {
const pageNum = i + 1;
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
if (cropData[pageNum]) {
const page = await cropperState.pdfDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 2.5 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
const finalCanvas = document.createElement('canvas');
const finalCtx = finalCanvas.getContext('2d');
const crop = cropData[pageNum];
const finalWidth = tempCanvas.width * crop.width;
const finalHeight = tempCanvas.height * crop.height;
finalCanvas.width = finalWidth;
finalCanvas.height = finalHeight;
finalCtx?.drawImage(
tempCanvas,
tempCanvas.width * crop.x,
tempCanvas.height * crop.y,
finalWidth,
finalHeight,
0, 0, finalWidth, finalHeight
);
const pngBytes = await new Promise<ArrayBuffer>((res) =>
finalCanvas.toBlob((blob) => blob?.arrayBuffer().then(res), 'image/jpeg', 0.9)
);
const embeddedImage = await newPdfDoc.embedPng(pngBytes);
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight });
} else {
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
newPdfDoc.addPage(copiedPage);
}
}
return newPdfDoc.save();
}
function resetState() {
if (cropperState.cropper) {
cropperState.cropper.destroy();
cropperState.cropper = null;
}
cropperState.pdfDoc = null;
cropperState.originalPdfBytes = null;
cropperState.pageCrops = {};
cropperState.currentPageNum = 1;
cropperState.file = null;
const cropperEditor = document.getElementById('cropper-editor');
if (cropperEditor) cropperEditor.classList.add('hidden');
const container = document.getElementById('cropper-container');
if (container) container.innerHTML = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const cropBtn = document.getElementById('crop-button') as HTMLButtonElement;
if (cropBtn) cropBtn.disabled = true;
}

View File

@@ -0,0 +1,239 @@
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
interface PageState {
file: File | null;
}
const pageState: PageState = {
file: null,
};
function resetState() {
pageState.file = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const passwordInput = document.getElementById('password-input') as HTMLInputElement;
if (passwordInput) passwordInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
async function decryptPdf() {
if (!pageState.file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
const password = (document.getElementById('password-input') as HTMLInputElement)?.value;
if (!password) {
showAlert('Input Required', 'Please enter the PDF password.');
return;
}
const inputPath = '/input.pdf';
const outputPath = '/output.pdf';
let qpdf: any;
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
try {
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Initializing decryption...';
qpdf = await initializeQpdf();
if (loaderText) loaderText.textContent = 'Reading encrypted PDF...';
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
if (loaderText) loaderText.textContent = 'Decrypting PDF...';
const args = [inputPath, '--password=' + password, '--decrypt', outputPath];
try {
qpdf.callMain(args);
} catch (qpdfError: any) {
console.error('qpdf execution error:', qpdfError);
if (
qpdfError.message?.includes('invalid password') ||
qpdfError.message?.includes('password')
) {
throw new Error('INVALID_PASSWORD');
}
throw qpdfError;
}
if (loaderText) loaderText.textContent = 'Preparing download...';
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (outputFile.length === 0) {
throw new Error('Decryption resulted in an empty file.');
}
const blob = new Blob([outputFile], { type: 'application/pdf' });
downloadFile(blob, `unlocked-${pageState.file.name}`);
if (loaderModal) loaderModal.classList.add('hidden');
showAlert(
'Success',
'PDF decrypted successfully! Your download has started.',
'success',
() => { resetState(); }
);
} catch (error: any) {
console.error('Error during PDF decryption:', error);
if (loaderModal) loaderModal.classList.add('hidden');
if (error.message === 'INVALID_PASSWORD') {
showAlert(
'Incorrect Password',
'The password you entered is incorrect. Please try again.'
);
} else if (error.message?.includes('password')) {
showAlert(
'Password Error',
'Unable to decrypt the PDF with the provided password.'
);
} else {
showAlert(
'Decryption Failed',
`An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}`
);
}
} finally {
try {
if (qpdf?.FS) {
try {
qpdf.FS.unlink(inputPath);
} catch (e) {
console.warn('Failed to unlink input file:', e);
}
try {
qpdf.FS.unlink(outputPath);
} catch (e) {
console.warn('Failed to unlink output file:', e);
}
}
} catch (cleanupError) {
console.warn('Failed to cleanup WASM FS:', cleanupError);
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', decryptPdf);
}
});

View File

@@ -1,105 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
initializeQpdf,
readFileAsArrayBuffer,
} from '../utils/helpers.js';
import { state } from '../state.js';
export async function decrypt() {
const file = state.files[0];
const password = (
document.getElementById('password-input') as HTMLInputElement
)?.value;
if (!password) {
showAlert('Input Required', 'Please enter the PDF password.');
return;
}
const inputPath = '/input.pdf';
const outputPath = '/output.pdf';
let qpdf: any;
try {
showLoader('Initializing decryption...');
qpdf = await initializeQpdf();
showLoader('Reading encrypted PDF...');
const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
showLoader('Decrypting PDF...');
const args = [inputPath, '--password=' + password, '--decrypt', outputPath];
try {
qpdf.callMain(args);
} catch (qpdfError: any) {
console.error('qpdf execution error:', qpdfError);
if (
qpdfError.message?.includes('invalid password') ||
qpdfError.message?.includes('password')
) {
throw new Error('INVALID_PASSWORD');
}
throw qpdfError;
}
showLoader('Preparing download...');
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (outputFile.length === 0) {
throw new Error('Decryption resulted in an empty file.');
}
const blob = new Blob([outputFile], { type: 'application/pdf' });
downloadFile(blob, `unlocked-${file.name}`);
hideLoader();
showAlert(
'Success',
'PDF decrypted successfully! Your download has started.'
);
} catch (error: any) {
console.error('Error during PDF decryption:', error);
hideLoader();
if (error.message === 'INVALID_PASSWORD') {
showAlert(
'Incorrect Password',
'The password you entered is incorrect. Please try again.'
);
} else if (error.message?.includes('password')) {
showAlert(
'Password Error',
'Unable to decrypt the PDF with the provided password.'
);
} else {
showAlert(
'Decryption Failed',
`An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}`
);
}
} finally {
try {
if (qpdf?.FS) {
try {
qpdf.FS.unlink(inputPath);
} catch (e) {
console.warn('Failed to unlink input file:', e);
}
try {
qpdf.FS.unlink(outputPath);
} catch (e) {
console.warn('Failed to unlink output file:', e);
}
}
} catch (cleanupError) {
console.warn('Failed to cleanup WASM FS:', cleanupError);
}
}
}

View File

@@ -0,0 +1,274 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument, parsePageRanges } from '../utils/helpers.js';
import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface DeleteState {
file: File | null;
pdfDoc: any;
pdfJsDoc: any;
totalPages: number;
pagesToDelete: Set<number>;
}
const deleteState: DeleteState = {
file: null,
pdfDoc: null,
pdfJsDoc: null,
totalPages: 0,
pagesToDelete: new Set(),
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const pagesInput = document.getElementById('pages-to-delete') as HTMLInputElement;
if (fileInput) fileInput.addEventListener('change', handleFileUpload);
if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (processBtn) processBtn.addEventListener('click', deletePages);
if (pagesInput) pagesInput.addEventListener('input', updatePreview);
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) handleFile(input.files[0]);
}
async function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
showLoader('Loading PDF...');
deleteState.file = file;
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
deleteState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
deleteState.pdfJsDoc = await getPDFDocument({ data: (arrayBuffer as ArrayBuffer).slice(0) }).promise;
deleteState.totalPages = deleteState.pdfDoc.getPageCount();
deleteState.pagesToDelete = new Set();
updateFileDisplay();
showOptions();
await renderThumbnails();
hideLoader();
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
}
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !deleteState.file) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = deleteState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(deleteState.file.size)}${deleteState.totalPages} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => resetState();
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function showOptions() {
const deleteOptions = document.getElementById('delete-options');
const totalPagesSpan = document.getElementById('total-pages');
if (deleteOptions) deleteOptions.classList.remove('hidden');
if (totalPagesSpan) totalPagesSpan.textContent = deleteState.totalPages.toString();
}
async function renderThumbnails() {
const container = document.getElementById('delete-pages-preview');
if (!container) return;
container.innerHTML = '';
for (let i = 1; i <= deleteState.totalPages; i++) {
const page = await deleteState.pdfJsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.3 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
const wrapper = document.createElement('div');
wrapper.className = 'relative cursor-pointer group';
wrapper.dataset.page = i.toString();
const imgContainer = document.createElement('div');
imgContainer.className = 'w-full h-28 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'max-w-full max-h-full object-contain';
const pageLabel = document.createElement('span');
pageLabel.className = 'absolute top-1 left-1 bg-gray-800 text-white text-xs px-1.5 py-0.5 rounded';
pageLabel.textContent = `${i}`;
const deleteOverlay = document.createElement('div');
deleteOverlay.className = 'absolute inset-0 bg-red-500/50 hidden items-center justify-center rounded-lg';
deleteOverlay.innerHTML = '<i data-lucide="x" class="w-8 h-8 text-white"></i>';
imgContainer.appendChild(img);
wrapper.append(imgContainer, pageLabel, deleteOverlay);
container.appendChild(wrapper);
wrapper.addEventListener('click', () => togglePageDelete(i, wrapper));
}
createIcons({ icons });
}
function togglePageDelete(pageNum: number, wrapper: HTMLElement) {
const overlay = wrapper.querySelector('.bg-red-500\\/50');
if (deleteState.pagesToDelete.has(pageNum)) {
deleteState.pagesToDelete.delete(pageNum);
overlay?.classList.add('hidden');
overlay?.classList.remove('flex');
} else {
deleteState.pagesToDelete.add(pageNum);
overlay?.classList.remove('hidden');
overlay?.classList.add('flex');
}
updateInputFromSelection();
}
function updateInputFromSelection() {
const pagesInput = document.getElementById('pages-to-delete') as HTMLInputElement;
if (pagesInput) {
const sorted = Array.from(deleteState.pagesToDelete).sort((a, b) => a - b);
pagesInput.value = sorted.join(', ');
}
}
function updatePreview() {
const pagesInput = document.getElementById('pages-to-delete') as HTMLInputElement;
if (!pagesInput) return;
deleteState.pagesToDelete = new Set(parsePageRanges(pagesInput.value, deleteState.totalPages).map(i => i + 1));
const container = document.getElementById('delete-pages-preview');
if (!container) return;
container.querySelectorAll('[data-page]').forEach((wrapper) => {
const pageNum = parseInt((wrapper as HTMLElement).dataset.page || '0', 10);
const overlay = wrapper.querySelector('.bg-red-500\\/50');
if (deleteState.pagesToDelete.has(pageNum)) {
overlay?.classList.remove('hidden');
overlay?.classList.add('flex');
} else {
overlay?.classList.add('hidden');
overlay?.classList.remove('flex');
}
});
}
async function deletePages() {
if (deleteState.pagesToDelete.size === 0) {
showAlert('No Pages', 'Please select pages to delete.');
return;
}
if (deleteState.pagesToDelete.size >= deleteState.totalPages) {
showAlert('Error', 'Cannot delete all pages.');
return;
}
showLoader('Deleting pages...');
try {
const pagesToKeep = [];
for (let i = 0; i < deleteState.totalPages; i++) {
if (!deleteState.pagesToDelete.has(i + 1)) pagesToKeep.push(i);
}
const newPdf = await PDFDocument.create();
const copiedPages = await newPdf.copyPages(deleteState.pdfDoc, pagesToKeep);
copiedPages.forEach(page => newPdf.addPage(page));
const pdfBytes = await newPdf.save();
const baseName = deleteState.file?.name.replace('.pdf', '') || 'document';
downloadFile(new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }), `${baseName}_pages_removed.pdf`);
hideLoader();
showAlert('Success', `Deleted ${deleteState.pagesToDelete.size} page(s) successfully!`, 'success', () => resetState());
} catch (error) {
console.error('Error deleting pages:', error);
hideLoader();
showAlert('Error', 'Failed to delete pages.');
}
}
function resetState() {
deleteState.file = null;
deleteState.pdfDoc = null;
deleteState.pdfJsDoc = null;
deleteState.totalPages = 0;
deleteState.pagesToDelete = new Set();
document.getElementById('delete-options')?.classList.add('hidden');
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const pagesInput = document.getElementById('pages-to-delete') as HTMLInputElement;
if (pagesInput) pagesInput.value = '';
const container = document.getElementById('delete-pages-preview');
if (container) container.innerHTML = '';
}

View File

@@ -1,111 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function deletePages() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageInput = document.getElementById('pages-to-delete').value;
if (!pageInput) {
showAlert('Invalid Input', 'Please enter page numbers to delete.');
return;
}
showLoader('Deleting pages...');
try {
const totalPages = state.pdfDoc.getPageCount();
const indicesToDelete = new Set();
const ranges = pageInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
)
continue;
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToDelete.add(pageNum - 1);
}
}
if (indicesToDelete.size === 0) {
showAlert('Invalid Input', 'No valid pages selected for deletion.');
hideLoader();
return;
}
if (indicesToDelete.size >= totalPages) {
showAlert('Invalid Input', 'You cannot delete all pages.');
hideLoader();
return;
}
const indicesToKeep = Array.from(
{ length: totalPages },
(_, i) => i
).filter((index) => !indicesToDelete.has(index));
const newPdf = await PDFLibDocument.create();
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'deleted-pages.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not delete pages.');
} finally {
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();
}

View File

@@ -0,0 +1,212 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
interface DividePagesState {
file: File | null;
pdfDoc: PDFLibDocument | null;
}
const pageState: DividePagesState = {
file: null,
pdfDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
if (splitTypeSelect) splitTypeSelect.value = 'vertical';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function dividePages() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
const splitTypeSelect = document.getElementById('split-type') as HTMLSelectElement;
const splitType = splitTypeSelect.value;
showLoader('Splitting PDF pages...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pages = pageState.pdfDoc.getPages();
for (let i = 0; i < pages.length; i++) {
const originalPage = pages[i];
const { width, height } = originalPage.getSize();
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
const [page1] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
const [page2] = await newPdfDoc.copyPages(pageState.pdfDoc, [i]);
switch (splitType) {
case 'vertical':
page1.setCropBox(0, 0, width / 2, height);
page2.setCropBox(width / 2, 0, width / 2, height);
break;
case 'horizontal':
page1.setCropBox(0, height / 2, width, height / 2);
page2.setCropBox(0, 0, width, height / 2);
break;
}
newPdfDoc.addPage(page1);
newPdfDoc.addPage(page2);
}
const newPdfBytes = await newPdfDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_divided.pdf`
);
showAlert('Success', 'Pages have been divided successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while dividing the PDF.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', dividePages);
}
});

View File

@@ -0,0 +1,365 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
const worker = new Worker(import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js');
interface AttachmentInfo {
index: number;
name: string;
page: number;
data: Uint8Array;
}
interface EditAttachmentState {
file: File | null;
allAttachments: AttachmentInfo[];
attachmentsToRemove: Set<number>;
}
const pageState: EditAttachmentState = {
file: null,
allAttachments: [],
attachmentsToRemove: new Set(),
};
function resetState() {
pageState.file = null;
pageState.allAttachments = [];
pageState.attachmentsToRemove.clear();
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const attachmentsList = document.getElementById('attachments-list');
if (attachmentsList) attachmentsList.innerHTML = '';
const processBtn = document.getElementById('process-btn');
if (processBtn) processBtn.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
worker.onmessage = function (e) {
const data = e.data;
if (data.status === 'success' && data.attachments !== undefined) {
pageState.allAttachments = data.attachments.map(function (att: any) {
return {
...att,
data: new Uint8Array(att.data)
};
});
displayAttachments(data.attachments);
hideLoader();
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
const originalName = pageState.file?.name.replace(/\.pdf$/i, '') || 'document';
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`${originalName}_edited.pdf`
);
showAlert('Success', 'Attachments updated successfully!', 'success', function () {
resetState();
});
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
}
};
worker.onerror = function (error) {
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
};
function displayAttachments(attachments: AttachmentInfo[]) {
const attachmentsList = document.getElementById('attachments-list');
const processBtn = document.getElementById('process-btn');
if (!attachmentsList) return;
attachmentsList.innerHTML = '';
if (attachments.length === 0) {
const noAttachments = document.createElement('p');
noAttachments.className = 'text-gray-400 text-center py-4';
noAttachments.textContent = 'No attachments found in this PDF.';
attachmentsList.appendChild(noAttachments);
return;
}
// Controls container
const controlsContainer = document.createElement('div');
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
const removeAllBtn = document.createElement('button');
removeAllBtn.className = 'bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
removeAllBtn.textContent = 'Remove All Attachments';
removeAllBtn.onclick = function () {
if (pageState.allAttachments.length === 0) return;
const allSelected = pageState.allAttachments.every(function (attachment) {
return pageState.attachmentsToRemove.has(attachment.index);
});
if (allSelected) {
pageState.allAttachments.forEach(function (attachment) {
pageState.attachmentsToRemove.delete(attachment.index);
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
if (element) {
element.classList.remove('opacity-50', 'line-through');
const btn = element.querySelector('button');
if (btn) {
btn.classList.remove('bg-gray-600');
btn.classList.add('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Remove All Attachments';
} else {
pageState.allAttachments.forEach(function (attachment) {
pageState.attachmentsToRemove.add(attachment.index);
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
if (element) {
element.classList.add('opacity-50', 'line-through');
const btn = element.querySelector('button');
if (btn) {
btn.classList.add('bg-gray-600');
btn.classList.remove('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Deselect All';
}
};
controlsContainer.appendChild(removeAllBtn);
attachmentsList.appendChild(controlsContainer);
// Attachment items
for (const attachment of attachments) {
const attachmentDiv = document.createElement('div');
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
const infoDiv = document.createElement('div');
infoDiv.className = 'flex-1';
const nameSpan = document.createElement('span');
nameSpan.className = 'text-white font-medium block';
nameSpan.textContent = attachment.name;
const levelSpan = document.createElement('span');
levelSpan.className = 'text-gray-400 text-sm block';
if (attachment.page === 0) {
levelSpan.textContent = 'Document-level attachment';
} else {
levelSpan.textContent = `Page ${attachment.page} attachment`;
}
infoDiv.append(nameSpan, levelSpan);
const actionsDiv = document.createElement('div');
actionsDiv.className = 'flex items-center gap-2';
const removeBtn = document.createElement('button');
removeBtn.className = `${pageState.attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.title = 'Remove attachment';
removeBtn.onclick = function () {
if (pageState.attachmentsToRemove.has(attachment.index)) {
pageState.attachmentsToRemove.delete(attachment.index);
attachmentDiv.classList.remove('opacity-50', 'line-through');
removeBtn.classList.remove('bg-gray-600');
removeBtn.classList.add('bg-red-600');
} else {
pageState.attachmentsToRemove.add(attachment.index);
attachmentDiv.classList.add('opacity-50', 'line-through');
removeBtn.classList.add('bg-gray-600');
removeBtn.classList.remove('bg-red-600');
}
const allSelected = pageState.allAttachments.every(function (att) {
return pageState.attachmentsToRemove.has(att.index);
});
removeAllBtn.textContent = allSelected ? 'Deselect All' : 'Remove All Attachments';
};
actionsDiv.append(removeBtn);
attachmentDiv.append(infoDiv, actionsDiv);
attachmentsList.appendChild(attachmentDiv);
}
createIcons({ icons });
if (processBtn) processBtn.classList.remove('hidden');
}
async function loadAttachments() {
if (!pageState.file) return;
showLoader('Loading attachments...');
try {
const fileBuffer = await pageState.file.arrayBuffer();
const message = {
command: 'get-attachments',
fileBuffer: fileBuffer,
fileName: pageState.file.name
};
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error loading attachments:', error);
hideLoader();
showAlert('Error', 'Failed to load attachments from PDF.');
}
}
async function saveChanges() {
if (!pageState.file) {
showAlert('Error', 'No PDF file loaded.');
return;
}
if (pageState.attachmentsToRemove.size === 0) {
showAlert('No Changes', 'No attachments selected for removal.');
return;
}
showLoader('Processing attachments...');
try {
const fileBuffer = await pageState.file.arrayBuffer();
const message = {
command: 'edit-attachments',
fileBuffer: fileBuffer,
fileName: pageState.file.name,
attachmentsToRemove: Array.from(pageState.attachmentsToRemove)
};
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error editing attachments:', error);
hideLoader();
showAlert('Error', 'Failed to edit attachments.');
}
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
await loadAttachments();
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', saveChanges);
}
});

View File

@@ -1,218 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
const worker = new Worker(import.meta.env.BASE_URL + 'workers/edit-attachments.worker.js');
let allAttachments: Array<{ index: number; name: string; page: number; data: Uint8Array }> = [];
let attachmentsToRemove: Set<number> = new Set();
export async function setupEditAttachmentsTool() {
const optionsDiv = document.getElementById('edit-attachments-options');
if (!optionsDiv || !state.files || state.files.length === 0) return;
optionsDiv.classList.remove('hidden');
await loadAttachmentsList();
}
async function loadAttachmentsList() {
const attachmentsList = document.getElementById('attachments-list');
if (!attachmentsList || !state.files || state.files.length === 0) return;
attachmentsList.innerHTML = '';
attachmentsToRemove.clear();
allAttachments = [];
try {
showLoader('Loading attachments...');
const file = state.files[0];
const fileBuffer = await readFileAsArrayBuffer(file);
const message = {
command: 'get-attachments',
fileBuffer: fileBuffer,
fileName: file.name
};
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error loading attachments:', error);
hideLoader();
showAlert('Error', 'Failed to load attachments from PDF.');
}
}
worker.onmessage = (e) => {
const data = e.data;
if (data.status === 'success' && data.attachments !== undefined) {
const attachments = data.attachments;
allAttachments = attachments.map(att => ({
...att,
data: new Uint8Array(att.data)
}));
displayAttachments(attachments);
hideLoader();
} else if (data.status === 'success' && data.modifiedPDF !== undefined) {
hideLoader();
downloadFile(
new Blob([new Uint8Array(data.modifiedPDF)], { type: 'application/pdf' }),
`edited-attachments-${data.fileName}`
);
showAlert('Success', 'Attachments updated successfully!');
} else if (data.status === 'error') {
hideLoader();
showAlert('Error', data.message || 'Unknown error occurred.');
}
};
worker.onerror = (error) => {
hideLoader();
console.error('Worker error:', error);
showAlert('Error', 'Worker error occurred. Check console for details.');
};
function displayAttachments(attachments) {
const attachmentsList = document.getElementById('attachments-list');
if (!attachmentsList) return;
const existingControls = attachmentsList.querySelector('.attachments-controls');
attachmentsList.innerHTML = '';
if (existingControls) {
attachmentsList.appendChild(existingControls);
}
if (attachments.length === 0) {
const noAttachments = document.createElement('p');
noAttachments.className = 'text-gray-400 text-center py-4';
noAttachments.textContent = 'No attachments found in this PDF.';
attachmentsList.appendChild(noAttachments);
return;
}
const controlsContainer = document.createElement('div');
controlsContainer.className = 'attachments-controls mb-4 flex justify-end';
const removeAllBtn = document.createElement('button');
removeAllBtn.className = 'btn bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded text-sm';
removeAllBtn.textContent = 'Remove All Attachments';
removeAllBtn.onclick = () => {
if (allAttachments.length === 0) return;
const allSelected = allAttachments.every(attachment => attachmentsToRemove.has(attachment.index));
if (allSelected) {
allAttachments.forEach(attachment => {
attachmentsToRemove.delete(attachment.index);
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
if (element) {
element.classList.remove('opacity-50', 'line-through');
const removeBtn = element.querySelector('button');
if (removeBtn) {
removeBtn.classList.remove('bg-gray-600');
removeBtn.classList.add('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Remove All Attachments';
} else {
allAttachments.forEach(attachment => {
attachmentsToRemove.add(attachment.index);
const element = document.querySelector(`[data-attachment-index="${attachment.index}"]`);
if (element) {
element.classList.add('opacity-50', 'line-through');
const removeBtn = element.querySelector('button');
if (removeBtn) {
removeBtn.classList.add('bg-gray-600');
removeBtn.classList.remove('bg-red-600');
}
}
});
removeAllBtn.textContent = 'Deselect All';
}
};
controlsContainer.appendChild(removeAllBtn);
attachmentsList.appendChild(controlsContainer);
for (const attachment of attachments) {
const attachmentDiv = document.createElement('div');
attachmentDiv.className = 'flex items-center justify-between p-3 bg-gray-800 rounded-lg border border-gray-700';
attachmentDiv.dataset.attachmentIndex = attachment.index.toString();
const infoDiv = document.createElement('div');
infoDiv.className = 'flex-1';
const nameSpan = document.createElement('span');
nameSpan.className = 'text-white font-medium block';
nameSpan.textContent = attachment.name;
const levelSpan = document.createElement('span');
levelSpan.className = 'text-gray-400 text-sm block';
if (attachment.page === 0) {
levelSpan.textContent = 'Document-level attachment';
} else {
levelSpan.textContent = `Page ${attachment.page} attachment`;
}
infoDiv.append(nameSpan, levelSpan);
const actionsDiv = document.createElement('div');
actionsDiv.className = 'flex items-center gap-2';
const removeBtn = document.createElement('button');
removeBtn.className = `btn ${attachmentsToRemove.has(attachment.index) ? 'bg-gray-600' : 'bg-red-600'} hover:bg-red-700 text-white px-3 py-1 rounded text-sm`;
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.title = 'Remove attachment';
removeBtn.onclick = () => {
if (attachmentsToRemove.has(attachment.index)) {
attachmentsToRemove.delete(attachment.index);
attachmentDiv.classList.remove('opacity-50', 'line-through');
removeBtn.classList.remove('bg-gray-600');
removeBtn.classList.add('bg-red-600');
} else {
attachmentsToRemove.add(attachment.index);
attachmentDiv.classList.add('opacity-50', 'line-through');
removeBtn.classList.add('bg-gray-600');
removeBtn.classList.remove('bg-red-600');
}
const allSelected = allAttachments.every(attachment => attachmentsToRemove.has(attachment.index));
removeAllBtn.textContent = allSelected ? 'Deselect All' : 'Remove All Attachments';
};
actionsDiv.append(removeBtn);
attachmentDiv.append(infoDiv, actionsDiv);
attachmentsList.appendChild(attachmentDiv);
}
}
export async function editAttachments() {
if (!state.files || state.files.length === 0) {
showAlert('Error', 'No PDF file loaded.');
return;
}
showLoader('Processing attachments...');
try {
const file = state.files[0];
const fileBuffer = await readFileAsArrayBuffer(file);
const message = {
command: 'edit-attachments',
fileBuffer: fileBuffer,
fileName: file.name,
attachmentsToRemove: Array.from(attachmentsToRemove)
};
worker.postMessage(message, [fileBuffer]);
} catch (error) {
console.error('Error editing attachments:', error);
hideLoader();
showAlert('Error', 'Failed to edit attachments.');
}
}

View File

@@ -0,0 +1,373 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, PDFName, PDFString } from 'pdf-lib';
interface EditMetadataState {
file: File | null;
pdfDoc: PDFLibDocument | null;
}
const pageState: EditMetadataState = {
file: null,
pdfDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
// Clear form fields
const fields = ['meta-title', 'meta-author', 'meta-subject', 'meta-keywords', 'meta-creator', 'meta-producer', 'meta-creation-date', 'meta-mod-date'];
fields.forEach(function (fieldId) {
const field = document.getElementById(fieldId) as HTMLInputElement;
if (field) field.value = '';
});
// Clear custom fields
const customFieldsContainer = document.getElementById('custom-fields-container');
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
}
function formatDateForInput(date: Date | undefined): string {
if (!date) return '';
try {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day}T${hours}:${minutes}`;
} catch {
return '';
}
}
function addCustomFieldRow(key: string = '', value: string = '') {
const container = document.getElementById('custom-fields-container');
if (!container) return;
const row = document.createElement('div');
row.className = 'flex flex-col gap-2';
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Key (e.g., Department)';
keyInput.value = key;
keyInput.className = 'custom-meta-key w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Value (e.g., Marketing)';
valueInput.value = value;
valueInput.className = 'custom-meta-value w-full bg-gray-700 border border-gray-600 text-white text-sm rounded-lg focus:ring-indigo-500 focus:border-indigo-500 p-2.5';
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.className = 'text-red-400 hover:text-red-300 p-2 self-center';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-5 h-5"></i>';
removeBtn.onclick = function () {
row.remove();
};
row.append(keyInput, valueInput, removeBtn);
container.appendChild(row);
createIcons({ icons });
}
function populateMetadataFields() {
if (!pageState.pdfDoc) return;
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
const authorInput = document.getElementById('meta-author') as HTMLInputElement;
const subjectInput = document.getElementById('meta-subject') as HTMLInputElement;
const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement;
const creatorInput = document.getElementById('meta-creator') as HTMLInputElement;
const producerInput = document.getElementById('meta-producer') as HTMLInputElement;
const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement;
const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement;
if (titleInput) titleInput.value = pageState.pdfDoc.getTitle() || '';
if (authorInput) authorInput.value = pageState.pdfDoc.getAuthor() || '';
if (subjectInput) subjectInput.value = pageState.pdfDoc.getSubject() || '';
if (keywordsInput) keywordsInput.value = pageState.pdfDoc.getKeywords() || '';
if (creatorInput) creatorInput.value = pageState.pdfDoc.getCreator() || '';
if (producerInput) producerInput.value = pageState.pdfDoc.getProducer() || '';
if (creationDateInput) creationDateInput.value = formatDateForInput(pageState.pdfDoc.getCreationDate());
if (modDateInput) modDateInput.value = formatDateForInput(pageState.pdfDoc.getModificationDate());
// Load custom fields
const customFieldsContainer = document.getElementById('custom-fields-container');
if (customFieldsContainer) customFieldsContainer.innerHTML = '';
try {
// @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pageState.pdfDoc.getInfoDict();
const standardKeys = new Set([
'Title', 'Author', 'Subject', 'Keywords', 'Creator',
'Producer', 'CreationDate', 'ModDate'
]);
const allKeys = infoDict
.keys()
.map(function (key: { asString: () => string }) {
return key.asString().substring(1);
});
allKeys.forEach(function (key: string) {
if (!standardKeys.has(key)) {
const rawValue = infoDict.lookup(key);
let displayValue = '';
if (rawValue && typeof rawValue.decodeText === 'function') {
displayValue = rawValue.decodeText();
} else if (rawValue && typeof rawValue.asString === 'function') {
displayValue = rawValue.asString();
} else if (rawValue) {
displayValue = String(rawValue);
}
addCustomFieldRow(key, displayValue);
}
});
} catch (e) {
console.warn('Could not read custom metadata fields:', e);
}
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
populateMetadataFields();
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function saveMetadata() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
showLoader('Updating metadata...');
try {
const titleInput = document.getElementById('meta-title') as HTMLInputElement;
const authorInput = document.getElementById('meta-author') as HTMLInputElement;
const subjectInput = document.getElementById('meta-subject') as HTMLInputElement;
const keywordsInput = document.getElementById('meta-keywords') as HTMLInputElement;
const creatorInput = document.getElementById('meta-creator') as HTMLInputElement;
const producerInput = document.getElementById('meta-producer') as HTMLInputElement;
const creationDateInput = document.getElementById('meta-creation-date') as HTMLInputElement;
const modDateInput = document.getElementById('meta-mod-date') as HTMLInputElement;
pageState.pdfDoc.setTitle(titleInput.value);
pageState.pdfDoc.setAuthor(authorInput.value);
pageState.pdfDoc.setSubject(subjectInput.value);
pageState.pdfDoc.setCreator(creatorInput.value);
pageState.pdfDoc.setProducer(producerInput.value);
const keywords = keywordsInput.value;
pageState.pdfDoc.setKeywords(
keywords
.split(',')
.map(function (k) { return k.trim(); })
.filter(Boolean)
);
// Handle creation date
if (creationDateInput.value) {
pageState.pdfDoc.setCreationDate(new Date(creationDateInput.value));
}
// Handle modification date
if (modDateInput.value) {
pageState.pdfDoc.setModificationDate(new Date(modDateInput.value));
} else {
pageState.pdfDoc.setModificationDate(new Date());
}
// Handle custom fields
// @ts-expect-error getInfoDict is private but accessible at runtime
const infoDict = pageState.pdfDoc.getInfoDict();
const standardKeys = new Set([
'Title', 'Author', 'Subject', 'Keywords', 'Creator',
'Producer', 'CreationDate', 'ModDate'
]);
// Remove existing custom keys
const allKeys = infoDict
.keys()
.map(function (key: { asString: () => string }) {
return key.asString().substring(1);
});
allKeys.forEach(function (key: string) {
if (!standardKeys.has(key)) {
infoDict.delete(PDFName.of(key));
}
});
// Add new custom fields
const customKeys = document.querySelectorAll('.custom-meta-key');
const customValues = document.querySelectorAll('.custom-meta-value');
customKeys.forEach(function (keyInput, index) {
const key = (keyInput as HTMLInputElement).value.trim();
const value = (customValues[index] as HTMLInputElement).value.trim();
if (key && value) {
infoDict.set(PDFName.of(key), PDFString.of(value));
}
});
const newPdfBytes = await pageState.pdfDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_metadata-edited.pdf`
);
showAlert('Success', 'Metadata updated successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addCustomFieldBtn = document.getElementById('add-custom-field');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (addCustomFieldBtn) {
addCustomFieldBtn.addEventListener('click', function () {
addCustomFieldRow();
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', saveMetadata);
}
});

View File

@@ -1,93 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFName, PDFString } from 'pdf-lib';
export async function editMetadata() {
showLoader('Updating metadata...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const keywords = document.getElementById('meta-keywords').value;
state.pdfDoc.setKeywords(
keywords
.split(',')
.map((k: any) => k.trim())
.filter(Boolean)
);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const creationDate = document.getElementById('meta-creation-date').value;
if (creationDate) {
state.pdfDoc.setCreationDate(new Date(creationDate));
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const modDate = document.getElementById('meta-mod-date').value;
if (modDate) {
state.pdfDoc.setModificationDate(new Date(modDate));
} else {
state.pdfDoc.setModificationDate(new Date());
}
const infoDict = state.pdfDoc.getInfoDict();
const standardKeys = new Set([
'Title',
'Author',
'Subject',
'Keywords',
'Creator',
'Producer',
'CreationDate',
'ModDate',
]);
const allKeys = infoDict
.keys()
.map((key: any) => key.asString().substring(1)); // Clean keys
allKeys.forEach((key: any) => {
if (!standardKeys.has(key)) {
infoDict.delete(PDFName.of(key));
}
});
const customKeys = document.querySelectorAll('.custom-meta-key');
const customValues = document.querySelectorAll('.custom-meta-value');
customKeys.forEach((keyInput, index) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const key = keyInput.value.trim();
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const value = customValues[index].value.trim();
if (key && value) {
// Now we add the fields to a clean slate
infoDict.set(PDFName.of(key), PDFString.of(value));
}
});
const newPdfBytes = await state.pdfDoc.save();
downloadFile(
new Blob([newPdfBytes], { type: 'application/pdf' }),
'metadata-edited.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Could not update metadata. Please check that date formats are correct.'
);
} finally {
hideLoader();
}
}

View File

@@ -1,6 +1,7 @@
// Logic for PDF Editor Page
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { formatBytes } from '../utils/helpers.js';
let currentPdfUrl: string | null = null;
@@ -39,8 +40,9 @@ function initializePage() {
}
});
dropZone.addEventListener('click', () => {
fileInput?.click();
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
@@ -54,7 +56,6 @@ async function handleFileUpload(e: Event) {
if (input.files && input.files.length > 0) {
await handleFiles(input.files);
}
input.value = '';
}
async function handleFiles(files: FileList) {
@@ -71,11 +72,51 @@ async function handleFiles(files: FileList) {
const pdfContainer = document.getElementById('embed-pdf-container');
const uploader = document.getElementById('tool-uploader');
const dropZone = document.getElementById('drop-zone');
const fileDisplayArea = document.getElementById('file-display-area');
if (!pdfWrapper || !pdfContainer || !uploader || !dropZone) return;
if (!pdfWrapper || !pdfContainer || !uploader || !dropZone || !fileDisplayArea) return;
// Hide uploader elements but keep the container
dropZone.classList.add('hidden');
// Hide uploader elements but keep the container
// dropZone.classList.add('hidden');
// Show file display
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
if (currentPdfUrl) {
URL.revokeObjectURL(currentPdfUrl);
currentPdfUrl = null;
}
pdfContainer.textContent = '';
pdfWrapper.classList.add('hidden');
fileDisplayArea.innerHTML = '';
// dropZone.classList.remove('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
// Clear previous content
pdfContainer.textContent = '';

View File

@@ -0,0 +1,246 @@
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
interface PageState {
file: File | null;
}
const pageState: PageState = {
file: null,
};
function resetState() {
pageState.file = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const userPasswordInput = document.getElementById('user-password-input') as HTMLInputElement;
if (userPasswordInput) userPasswordInput.value = '';
const ownerPasswordInput = document.getElementById('owner-password-input') as HTMLInputElement;
if (ownerPasswordInput) ownerPasswordInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
async function encryptPdf() {
if (!pageState.file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
const userPassword = (document.getElementById('user-password-input') as HTMLInputElement)?.value || '';
const ownerPasswordInput = (document.getElementById('owner-password-input') as HTMLInputElement)?.value || '';
if (!userPassword) {
showAlert('Input Required', 'Please enter a user password.');
return;
}
const ownerPassword = ownerPasswordInput || userPassword;
const hasDistinctOwnerPassword = ownerPasswordInput !== '';
const inputPath = '/input.pdf';
const outputPath = '/output.pdf';
let qpdf: any;
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
try {
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Initializing encryption...';
qpdf = await initializeQpdf();
if (loaderText) loaderText.textContent = 'Reading PDF...';
const fileBuffer = await readFileAsArrayBuffer(pageState.file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
if (loaderText) loaderText.textContent = 'Encrypting PDF with 256-bit AES...';
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
// Only add restrictions if a distinct owner password was provided
if (hasDistinctOwnerPassword) {
args.push(
'--modify=none',
'--extract=n',
'--print=none',
'--accessibility=n',
'--annotate=n',
'--assemble=n',
'--form=n',
'--modify-other=n'
);
}
args.push('--', outputPath);
try {
qpdf.callMain(args);
} catch (qpdfError: any) {
console.error('qpdf execution error:', qpdfError);
throw new Error(
'Encryption failed: ' + (qpdfError.message || 'Unknown error')
);
}
if (loaderText) loaderText.textContent = 'Preparing download...';
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (!outputFile || outputFile.length === 0) {
throw new Error('Encryption resulted in an empty file.');
}
const blob = new Blob([outputFile], { type: 'application/pdf' });
downloadFile(blob, `encrypted-${pageState.file.name}`);
if (loaderModal) loaderModal.classList.add('hidden');
let successMessage = 'PDF encrypted successfully with 256-bit AES!';
if (!hasDistinctOwnerPassword) {
successMessage +=
' Note: Without a separate owner password, the PDF has no usage restrictions.';
}
showAlert('Success', successMessage, 'success', () => { resetState(); });
} catch (error: any) {
console.error('Error during PDF encryption:', error);
if (loaderModal) loaderModal.classList.add('hidden');
showAlert(
'Encryption Failed',
`An error occurred: ${error.message || 'The PDF might be corrupted.'}`
);
} finally {
try {
if (qpdf?.FS) {
try {
qpdf.FS.unlink(inputPath);
} catch (e) {
console.warn('Failed to unlink input file:', e);
}
try {
qpdf.FS.unlink(outputPath);
} catch (e) {
console.warn('Failed to unlink output file:', e);
}
}
} catch (cleanupError) {
console.warn('Failed to cleanup WASM FS:', cleanupError);
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', encryptPdf);
}
});

View File

@@ -1,113 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import {
downloadFile,
initializeQpdf,
readFileAsArrayBuffer,
} from '../utils/helpers.js';
import { state } from '../state.js';
export async function encrypt() {
const file = state.files[0];
const userPassword =
(document.getElementById('user-password-input') as HTMLInputElement)
?.value || '';
const ownerPasswordInput =
(document.getElementById('owner-password-input') as HTMLInputElement)
?.value || '';
if (!userPassword) {
showAlert('Input Required', 'Please enter a user password.');
return;
}
const ownerPassword = ownerPasswordInput || userPassword;
const hasDistinctOwnerPassword = ownerPasswordInput !== '';
const inputPath = '/input.pdf';
const outputPath = '/output.pdf';
let qpdf: any;
try {
showLoader('Initializing encryption...');
qpdf = await initializeQpdf();
showLoader('Reading PDF...');
const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
showLoader('Encrypting PDF with 256-bit AES...');
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
// Only add restrictions if a distinct owner password was provided
if (hasDistinctOwnerPassword) {
args.push(
'--modify=none',
'--extract=n',
'--print=none',
'--accessibility=n',
'--annotate=n',
'--assemble=n',
'--form=n',
'--modify-other=n'
);
}
args.push('--', outputPath);
try {
qpdf.callMain(args);
} catch (qpdfError: any) {
console.error('qpdf execution error:', qpdfError);
throw new Error(
'Encryption failed: ' + (qpdfError.message || 'Unknown error')
);
}
showLoader('Preparing download...');
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (!outputFile || outputFile.length === 0) {
throw new Error('Encryption resulted in an empty file.');
}
const blob = new Blob([outputFile], { type: 'application/pdf' });
downloadFile(blob, `encrypted-${file.name}`);
hideLoader();
let successMessage = 'PDF encrypted successfully with 256-bit AES!';
if (!hasDistinctOwnerPassword) {
successMessage +=
' Note: Without a separate owner password, the PDF has no usage restrictions.';
}
showAlert('Success', successMessage);
} catch (error: any) {
console.error('Error during PDF encryption:', error);
hideLoader();
showAlert(
'Encryption Failed',
`An error occurred: ${error.message || 'The PDF might be corrupted.'}`
);
} finally {
try {
if (qpdf?.FS) {
try {
qpdf.FS.unlink(inputPath);
} catch (e) {
console.warn('Failed to unlink input file:', e);
}
try {
qpdf.FS.unlink(outputPath);
} catch (e) {
console.warn('Failed to unlink output file:', e);
}
}
} catch (cleanupError) {
console.warn('Failed to cleanup WASM FS:', cleanupError);
}
}
}

View File

@@ -0,0 +1,260 @@
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import JSZip from 'jszip';
const worker = new Worker(import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js');
interface ExtractState {
files: File[];
}
const pageState: ExtractState = {
files: [],
};
function resetState() {
pageState.files = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const statusMessage = document.getElementById('status-message');
if (statusMessage) statusMessage.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
}
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
const statusMessage = document.getElementById('status-message') as HTMLElement;
if (!statusMessage) return;
statusMessage.textContent = message;
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
? 'bg-green-900 text-green-200'
: type === 'error'
? 'bg-red-900 text-red-200'
: 'bg-blue-900 text-blue-200'
}`;
statusMessage.classList.remove('hidden');
}
worker.onmessage = function (e) {
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
if (e.data.status === 'success') {
const attachments = e.data.attachments;
if (attachments.length === 0) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
resetState();
return;
}
const zip = new JSZip();
let totalSize = 0;
for (const attachment of attachments) {
zip.file(attachment.name, new Uint8Array(attachment.data));
totalSize += attachment.data.byteLength;
}
zip.generateAsync({ type: 'blob' }).then(function (zipBlob) {
downloadFile(zipBlob, 'extracted-attachments.zip');
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
showStatus(
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
'success'
);
resetState();
});
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
if (errorMessage.includes('No attachments were found')) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
resetState();
} else {
showStatus(`Error: ${errorMessage}`, 'error');
}
}
};
worker.onerror = function (error) {
console.error('Worker error:', error);
showStatus('Worker error occurred. Check console for details.', 'error');
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
};
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.files.length > 0) {
const summaryDiv = document.createElement('div');
summaryDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const countSpan = document.createElement('div');
countSpan.className = 'font-medium text-gray-200 text-sm mb-1';
countSpan.textContent = `${pageState.files.length} PDF file(s) selected`;
const sizeSpan = document.createElement('div');
sizeSpan.className = 'text-xs text-gray-400';
const totalSize = pageState.files.reduce(function (sum, f) { return sum + f.size; }, 0);
sizeSpan.textContent = formatBytes(totalSize);
infoContainer.append(countSpan, sizeSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
summaryDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(summaryDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function extractAttachments() {
if (pageState.files.length === 0) {
showStatus('No Files', 'error');
return;
}
const processBtn = document.getElementById('process-btn');
if (processBtn) {
processBtn.classList.add('opacity-50', 'cursor-not-allowed');
processBtn.setAttribute('disabled', 'true');
}
showStatus('Reading files...', 'info');
try {
const fileBuffers: ArrayBuffer[] = [];
const fileNames: string[] = [];
for (const file of pageState.files) {
const buffer = await file.arrayBuffer();
fileBuffers.push(buffer);
fileNames.push(file.name);
}
showStatus(`Extracting attachments from ${pageState.files.length} file(s)...`, 'info');
const message = {
command: 'extract-attachments',
fileBuffers,
fileNames,
};
const transferables = fileBuffers.map(function (buf) { return buf; });
worker.postMessage(message, transferables);
} catch (error) {
console.error('Error reading files:', error);
showStatus(
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
'error'
);
if (processBtn) {
processBtn.classList.remove('opacity-50', 'cursor-not-allowed');
processBtn.removeAttribute('disabled');
}
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const pdfFiles = Array.from(files).filter(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
pageState.files = pdfFiles;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
handleFileSelect(files);
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', extractAttachments);
}
});

View File

@@ -1,185 +0,0 @@
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import { showAlert } from '../ui.js';
import JSZip from 'jszip';
const worker = new Worker(import.meta.env.BASE_URL + 'workers/extract-attachments.worker.js');
interface ExtractAttachmentSuccessResponse {
status: 'success';
attachments: Array<{ name: string; data: ArrayBuffer }>;
}
interface ExtractAttachmentErrorResponse {
status: 'error';
message: string;
}
type ExtractAttachmentResponse = ExtractAttachmentSuccessResponse | ExtractAttachmentErrorResponse;
export async function extractAttachments() {
if (state.files.length === 0) {
showStatus('No Files', 'error');
return;
}
document.getElementById('process-btn')?.classList.add('opacity-50', 'cursor-not-allowed');
document.getElementById('process-btn')?.setAttribute('disabled', 'true');
showStatus('Reading files (Main Thread)...', 'info');
try {
const fileBuffers: ArrayBuffer[] = [];
const fileNames: string[] = [];
for (const file of state.files) {
const buffer = await file.arrayBuffer();
fileBuffers.push(buffer);
fileNames.push(file.name);
}
showStatus(`Extracting attachments from ${state.files.length} file(s)...`, 'info');
const message: ExtractAttachmentsMessage = {
command: 'extract-attachments',
fileBuffers,
fileNames,
};
const transferables = fileBuffers.map(buf => buf);
worker.postMessage(message, transferables);
} catch (error) {
console.error('Error reading files:', error);
showStatus(
`Error reading files: ${error instanceof Error ? error.message : 'Unknown error occurred'}`,
'error'
);
document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed');
document.getElementById('process-btn')?.removeAttribute('disabled');
}
}
worker.onmessage = (e: MessageEvent<ExtractAttachmentResponse>) => {
document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed');
document.getElementById('process-btn')?.removeAttribute('disabled');
if (e.data.status === 'success') {
const attachments = e.data.attachments;
if (attachments.length === 0) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
state.files = [];
state.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.add('hidden');
}
return;
}
const zip = new JSZip();
let totalSize = 0;
for (const attachment of attachments) {
zip.file(attachment.name, new Uint8Array(attachment.data));
totalSize += attachment.data.byteLength;
}
zip.generateAsync({ type: 'blob' }).then((zipBlob) => {
downloadFile(zipBlob, 'extracted-attachments.zip');
showAlert('Success', `${attachments.length} attachment(s) extracted successfully!`);
showStatus(
`Extraction completed! ${attachments.length} attachment(s) in zip file (${formatBytes(totalSize)}). Download started.`,
'success'
);
state.files = [];
state.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.add('hidden');
}
});
} else if (e.data.status === 'error') {
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
console.error('Worker Error:', errorMessage);
if (errorMessage.includes('No attachments were found')) {
showAlert('No Attachments', 'The PDF file(s) do not contain any attachments to extract.');
state.files = [];
state.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) {
fileInput.value = '';
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.add('hidden');
}
} else {
showStatus(`Error: ${errorMessage}`, 'error');
}
}
};
worker.onerror = (error) => {
console.error('Worker error:', error);
showStatus('Worker error occurred. Check console for details.', 'error');
document.getElementById('process-btn')?.classList.remove('opacity-50', 'cursor-not-allowed');
document.getElementById('process-btn')?.removeAttribute('disabled');
};
function showStatus(message: string, type: 'success' | 'error' | 'info' = 'info') {
const statusMessage = document.getElementById('status-message') as HTMLElement;
if (!statusMessage) return;
statusMessage.textContent = message;
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${type === 'success'
? 'bg-green-900 text-green-200'
: type === 'error'
? 'bg-red-900 text-red-200'
: 'bg-blue-900 text-blue-200'
}`;
statusMessage.classList.remove('hidden');
}
interface ExtractAttachmentsMessage {
command: 'extract-attachments';
fileBuffers: ArrayBuffer[];
fileNames: string[];
}

View File

@@ -0,0 +1,209 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { readFileAsArrayBuffer, formatBytes, downloadFile, parsePageRanges } from '../utils/helpers.js';
import { PDFDocument } from 'pdf-lib';
import JSZip from 'jszip';
interface ExtractState {
file: File | null;
pdfDoc: any;
totalPages: number;
}
const extractState: ExtractState = {
file: null,
pdfDoc: null,
totalPages: 0,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFile(droppedFiles[0]);
}
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', extractPages);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFile(input.files[0]);
}
}
async function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
showLoader('Loading PDF...');
extractState.file = file;
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
extractState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false,
});
extractState.totalPages = extractState.pdfDoc.getPageCount();
updateFileDisplay();
showOptions();
hideLoader();
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
}
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !extractState.file) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = extractState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(extractState.file.size)}${extractState.totalPages} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => resetState();
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function showOptions() {
const extractOptions = document.getElementById('extract-options');
const totalPagesSpan = document.getElementById('total-pages');
if (extractOptions) {
extractOptions.classList.remove('hidden');
}
if (totalPagesSpan) {
totalPagesSpan.textContent = extractState.totalPages.toString();
}
}
async function extractPages() {
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement;
if (!pagesInput || !pagesInput.value.trim()) {
showAlert('No Pages', 'Please enter page numbers to extract.');
return;
}
const pagesToExtract = parsePageRanges(pagesInput.value, extractState.totalPages).map(i => i + 1);
if (pagesToExtract.length === 0) {
showAlert('Invalid Pages', 'No valid page numbers found.');
return;
}
showLoader('Extracting pages...');
try {
const zip = new JSZip();
const baseName = extractState.file?.name.replace('.pdf', '') || 'document';
for (const pageNum of pagesToExtract) {
const newPdf = await PDFDocument.create();
const [copiedPage] = await newPdf.copyPages(extractState.pdfDoc, [pageNum - 1]);
newPdf.addPage(copiedPage);
const pdfBytes = await newPdf.save();
zip.file(`${baseName}_page_${pageNum}.pdf`, pdfBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, `${baseName}_extracted_pages.zip`);
hideLoader();
showAlert('Success', `Extracted ${pagesToExtract.length} page(s) successfully!`, 'success', () => {
resetState();
});
} catch (error) {
console.error('Error extracting pages:', error);
hideLoader();
showAlert('Error', 'Failed to extract pages.');
}
}
function resetState() {
extractState.file = null;
extractState.pdfDoc = null;
extractState.totalPages = 0;
const extractOptions = document.getElementById('extract-options');
if (extractOptions) {
extractOptions.classList.add('hidden');
}
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
fileDisplayArea.innerHTML = '';
}
const pagesInput = document.getElementById('pages-to-extract') as HTMLInputElement;
if (pagesInput) {
pagesInput.value = '';
}
}

View File

@@ -1,70 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function extractPages() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageInput = document.getElementById('pages-to-extract').value;
if (!pageInput.trim()) {
showAlert('Invalid Input', 'Please enter page numbers to extract.');
return;
}
showLoader('Extracting pages...');
try {
const totalPages = state.pdfDoc.getPageCount();
const indicesToExtract = new Set();
const ranges = pageInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (
isNaN(start) ||
isNaN(end) ||
start < 1 ||
end > totalPages ||
start > end
)
continue;
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToExtract.add(pageNum - 1);
}
}
if (indicesToExtract.size === 0) {
showAlert('Invalid Input', 'No valid pages selected for extraction.');
hideLoader();
return;
}
const zip = new JSZip();
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
const sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
for (const index of sortedIndices) {
const newPdf = await PDFLibDocument.create();
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
index as number,
]);
newPdf.addPage(copiedPage);
const newPdfBytes = await newPdf.save();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'extracted-pages.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not extract pages.');
} finally {
hideLoader();
}
}

View File

@@ -1,114 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
export function setupFixDimensionsUI() {
const targetSizeSelect = document.getElementById('target-size');
const customSizeWrapper = document.getElementById('custom-size-wrapper');
if (targetSizeSelect && customSizeWrapper) {
targetSizeSelect.addEventListener('change', () => {
customSizeWrapper.classList.toggle(
'hidden',
(targetSizeSelect as HTMLSelectElement).value !== 'Custom'
);
});
}
}
export async function fixDimensions() {
const targetSizeKey = (
document.getElementById('target-size') as HTMLSelectElement
).value;
const orientation = (
document.getElementById('orientation') as HTMLSelectElement
).value;
const scalingMode = (
document.querySelector(
'input[name="scaling-mode"]:checked'
) as HTMLInputElement
).value;
const backgroundColor = hexToRgb(
(document.getElementById('background-color') as HTMLInputElement).value
);
showLoader('Standardizing pages...');
try {
let targetWidth, targetHeight;
if (targetSizeKey === 'Custom') {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const width = parseFloat(document.getElementById('custom-width').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const height = parseFloat(document.getElementById('custom-height').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const units = document.getElementById('custom-units').value;
if (units === 'in') {
targetWidth = width * 72;
targetHeight = height * 72;
} else {
// mm
targetWidth = width * (72 / 25.4);
targetHeight = height * (72 / 25.4);
}
} else {
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
}
if (orientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
for (const sourcePage of sourceDoc.getPages()) {
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
const embeddedPage = await newDoc.embedPage(sourcePage);
const newPage = newDoc.addPage([targetWidth, targetHeight]);
newPage.drawRectangle({
x: 0,
y: 0,
width: targetWidth,
height: targetHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
const scaleX = targetWidth / sourceWidth;
const scaleY = targetHeight / sourceHeight;
const scale =
scalingMode === 'fit'
? Math.min(scaleX, scaleY)
: Math.max(scaleX, scaleY);
const scaledWidth = sourceWidth * scale;
const scaledHeight = sourceHeight * scale;
const x = (targetWidth - scaledWidth) / 2;
const y = (targetHeight - scaledHeight) / 2;
newPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'standardized.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while standardizing pages.');
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,233 @@
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
interface PageState {
file: File | null;
}
const pageState: PageState = {
file: null,
};
function resetState() {
pageState.file = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
async function fixPageSize() {
if (!pageState.file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
const targetSizeKey = (document.getElementById('target-size') as HTMLSelectElement).value;
const orientation = (document.getElementById('orientation') as HTMLSelectElement).value;
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
const backgroundColor = hexToRgb((document.getElementById('background-color') as HTMLInputElement).value);
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Standardizing pages...';
try {
let targetWidth, targetHeight;
if (targetSizeKey === 'Custom') {
const width = parseFloat((document.getElementById('custom-width') as HTMLInputElement).value);
const height = parseFloat((document.getElementById('custom-height') as HTMLInputElement).value);
const units = (document.getElementById('custom-units') as HTMLSelectElement).value;
if (units === 'in') {
targetWidth = width * 72;
targetHeight = height * 72;
} else {
// mm
targetWidth = width * (72 / 25.4);
targetHeight = height * (72 / 25.4);
}
} else {
[targetWidth, targetHeight] = PageSizes[targetSizeKey as keyof typeof PageSizes];
}
if (orientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const arrayBuffer = await pageState.file.arrayBuffer();
const sourceDoc = await PDFLibDocument.load(arrayBuffer);
const newDoc = await PDFLibDocument.create();
for (const sourcePage of sourceDoc.getPages()) {
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
const embeddedPage = await newDoc.embedPage(sourcePage);
const newPage = newDoc.addPage([targetWidth, targetHeight]);
newPage.drawRectangle({
x: 0,
y: 0,
width: targetWidth,
height: targetHeight,
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
});
const scaleX = targetWidth / sourceWidth;
const scaleY = targetHeight / sourceHeight;
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
const scaledWidth = sourceWidth * scale;
const scaledHeight = sourceHeight * scale;
const x = (targetWidth - scaledWidth) / 2;
const y = (targetHeight - scaledHeight) / 2;
newPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'standardized.pdf'
);
showAlert('Success', 'Page sizes standardized successfully!', 'success', () => { resetState(); });
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while standardizing pages.');
} finally {
if (loaderModal) loaderModal.classList.add('hidden');
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const targetSizeSelect = document.getElementById('target-size');
const customSizeWrapper = document.getElementById('custom-size-wrapper');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
// Setup custom size toggle
if (targetSizeSelect && customSizeWrapper) {
targetSizeSelect.addEventListener('change', function () {
customSizeWrapper.classList.toggle(
'hidden',
(targetSizeSelect as HTMLSelectElement).value !== 'Custom'
);
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', fixPageSize);
}
});

View File

@@ -0,0 +1,234 @@
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers.js';
import { PDFDocument } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
import JSZip from 'jszip';
interface PageState {
files: File[];
}
const pageState: PageState = {
files: [],
};
function flattenFormsInDoc(pdfDoc: PDFDocument) {
const form = pdfDoc.getForm();
form.flatten();
}
function resetState() {
pageState.files = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileControls = document.getElementById('file-controls');
if (fileControls) fileControls.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileControls = document.getElementById('file-controls');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.files.length > 0) {
pageState.files.forEach((file, index) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
pageState.files.splice(index, 1);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
if (fileControls) fileControls.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (fileControls) fileControls.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
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) {
pageState.files.push(...pdfFiles);
updateUI();
}
}
}
async function flattenPdf() {
if (pageState.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
try {
if (pageState.files.length === 1) {
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Flattening PDF...';
const file = pageState.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
try {
flattenFormsInDoc(pdfDoc);
} catch (e: any) {
if (e.message.includes('getForm')) {
// Ignore if no form found
} else {
throw e;
}
}
const newPdfBytes = await pdfDoc.save();
downloadFile(
new Blob([newPdfBytes as BlobPart], { type: 'application/pdf' }),
`flattened_${file.name}`
);
if (loaderModal) loaderModal.classList.add('hidden');
} else {
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Flattening multiple PDFs...';
const zip = new JSZip();
let processedCount = 0;
for (let i = 0; i < pageState.files.length; i++) {
const file = pageState.files[i];
if (loaderText) loaderText.textContent = `Flattening ${i + 1}/${pageState.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: any) {
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.`, 'success', () => { resetState(); });
} else {
showAlert('Error', 'No PDFs could be processed.');
}
if (loaderModal) loaderModal.classList.add('hidden');
}
} catch (e: any) {
console.error(e);
if (loaderModal) loaderModal.classList.add('hidden');
showAlert('Error', e.message || 'An unexpected error occurred.');
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
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', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', flattenPdf);
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', function () {
fileInput.value = '';
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', function () {
resetState();
});
}
});

View File

@@ -1,86 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument } from 'pdf-lib';
export function flattenFormsInDoc(pdfDoc) {
const form = pdfDoc.getForm();
form.flatten();
}
export async function flatten() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PDF file.');
return;
}
try {
if (state.files.length === 1) {
showLoader('Flattening PDF...');
const file = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
try {
flattenFormsInDoc(pdfDoc);
} catch (e) {
if (e.message.includes('getForm')) {
// Ignore if no form found
} else {
throw e;
}
}
const flattenedBytes = await pdfDoc.save();
downloadFile(
new Blob([flattenedBytes as any], { type: 'application/pdf' }),
`flattened_${file.name}`
);
hideLoader();
} else {
showLoader('Flattening multiple PDFs...');
const JSZip = (await import('jszip')).default;
const zip = new JSZip();
let processedCount = 0;
for (let i = 0; i < state.files.length; i++) {
const file = state.files[i];
showLoader(`Flattening ${i + 1}/${state.files.length}: ${file.name}...`);
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
const pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true });
try {
flattenFormsInDoc(pdfDoc);
} catch (e) {
if (e.message.includes('getForm')) {
// Ignore if no form found
} else {
throw e;
}
}
const flattenedBytes = await pdfDoc.save();
zip.file(`flattened_${file.name}`, flattenedBytes);
processedCount++;
} catch (e) {
console.error(`Error processing ${file.name}:`, e);
}
}
if (processedCount > 0) {
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'flattened_pdfs.zip');
showAlert('Success', `Processed ${processedCount} PDFs.`);
} else {
showAlert('Error', 'No PDFs could be processed.');
}
hideLoader();
}
} catch (e) {
console.error(e);
hideLoader();
showAlert('Error', e.message || 'An unexpected error occurred.');
}
}

View File

@@ -0,0 +1,255 @@
// Self-contained Form Filler logic for standalone page
import { createIcons, icons } from 'lucide';
import { getPDFDocument } from '../utils/helpers.js';
let viewerIframe: HTMLIFrameElement | null = null;
let viewerReady = false;
let currentFile: File | null = null;
// UI helpers
function showLoader(message: string = 'Processing...') {
const loader = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loader) loader.classList.remove('hidden');
if (loaderText) loaderText.textContent = message;
}
function hideLoader() {
const loader = document.getElementById('loader-modal');
if (loader) loader.classList.add('hidden');
}
function showAlert(title: string, message: string, type: string = 'error', callback?: () => void) {
const modal = document.getElementById('alert-modal');
const alertTitle = document.getElementById('alert-title');
const alertMessage = document.getElementById('alert-message');
const okBtn = document.getElementById('alert-ok');
if (alertTitle) alertTitle.textContent = title;
if (alertMessage) alertMessage.textContent = message;
if (modal) modal.classList.remove('hidden');
if (okBtn) {
const newOkBtn = okBtn.cloneNode(true) as HTMLElement;
okBtn.replaceWith(newOkBtn);
newOkBtn.addEventListener('click', () => {
modal?.classList.add('hidden');
if (callback) callback();
});
}
}
function updateFileDisplay() {
const displayArea = document.getElementById('file-display-area');
if (!displayArea || !currentFile) return;
const fileSize = currentFile.size < 1024 * 1024
? `${(currentFile.size / 1024).toFixed(1)} KB`
: `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`;
displayArea.innerHTML = `
<div class="bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors">
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0">
<p class="truncate font-medium text-white">${currentFile.name}</p>
<p class="text-gray-400 text-sm">${fileSize}</p>
</div>
<button id="remove-file" class="text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2" title="Remove file">
<i data-lucide="trash-2" class="w-4 h-4"></i>
</button>
</div>
</div>
`;
createIcons({ icons });
document.getElementById('remove-file')?.addEventListener('click', () => resetState());
}
function resetState() {
viewerIframe = null;
viewerReady = false;
currentFile = null;
const displayArea = document.getElementById('file-display-area');
if (displayArea) displayArea.innerHTML = '';
document.getElementById('form-filler-options')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
// Clear viewer
const viewerContainer = document.getElementById('pdf-viewer-container');
if (viewerContainer) {
viewerContainer.innerHTML = '';
viewerContainer.style.height = '';
viewerContainer.style.aspectRatio = '';
}
const toolUploader = document.getElementById('tool-uploader');
const isFullWidth = localStorage.getItem('fullWidthMode') === 'true';
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-6xl');
toolUploader.classList.add('max-w-2xl');
}
}
// File handling
async function handleFileUpload(file: File) {
if (!file || file.type !== 'application/pdf') {
showAlert('Error', 'Please upload a valid PDF file.');
return;
}
currentFile = file;
updateFileDisplay();
await setupFormViewer();
}
async function adjustViewerHeight(file: File) {
const viewerContainer = document.getElementById('pdf-viewer-container');
if (!viewerContainer) return;
try {
const arrayBuffer = await file.arrayBuffer();
const loadingTask = getPDFDocument({ data: arrayBuffer });
const pdf = await loadingTask.promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1 });
// Add ~50px for toolbar height
const aspectRatio = viewport.width / (viewport.height + 50);
viewerContainer.style.height = 'auto';
viewerContainer.style.aspectRatio = `${aspectRatio}`;
} catch (e) {
console.error('Error adjusting viewer height:', e);
viewerContainer.style.height = '80vh';
}
}
async function setupFormViewer() {
if (!currentFile) return;
showLoader('Loading PDF form...');
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
if (!pdfViewerContainer) {
console.error('PDF viewer container not found');
hideLoader();
return;
}
const toolUploader = document.getElementById('tool-uploader');
const isFullWidth = localStorage.getItem('fullWidthMode') === 'true';
if (toolUploader && !isFullWidth) {
toolUploader.classList.remove('max-w-2xl');
toolUploader.classList.add('max-w-6xl');
}
try {
// Apply dynamic height
await adjustViewerHeight(currentFile);
pdfViewerContainer.innerHTML = '';
const arrayBuffer = await currentFile.arrayBuffer();
const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
viewerIframe = document.createElement('iframe');
viewerIframe.src = `/pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`;
viewerIframe.style.width = '100%';
viewerIframe.style.height = '100%';
viewerIframe.style.border = 'none';
viewerIframe.onload = () => {
viewerReady = true;
hideLoader();
};
pdfViewerContainer.appendChild(viewerIframe);
const formFillerOptions = document.getElementById('form-filler-options');
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
} catch (e) {
console.error('Critical error setting up form filler:', e);
showAlert('Error', 'Failed to load PDF form viewer.');
hideLoader();
}
}
async function processAndDownloadForm() {
if (!viewerIframe || !viewerReady) {
showAlert('Viewer not ready', 'Please wait for the form to finish loading.');
return;
}
try {
const viewerWindow = viewerIframe.contentWindow;
if (!viewerWindow) {
console.error('Cannot access iframe window');
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
return;
}
const viewerDoc = viewerWindow.document;
if (!viewerDoc) {
console.error('Cannot access iframe document');
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
return;
}
const downloadBtn = viewerDoc.getElementById('downloadButton') as HTMLButtonElement | null;
if (downloadBtn) {
console.log('Clicking download button...');
downloadBtn.click();
} else {
console.error('Download button not found in viewer');
const secondaryDownload = viewerDoc.getElementById('secondaryDownload') as HTMLButtonElement | null;
if (secondaryDownload) {
console.log('Clicking secondary download button...');
secondaryDownload.click();
} else {
showAlert('Download', 'Please use the Download button in the PDF viewer toolbar above.');
}
}
} catch (e) {
console.error('Failed to trigger download:', e);
showAlert('Download', 'Cannot access viewer controls. Please use the Download button in the PDF viewer toolbar above.');
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
fileInput?.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) handleFileUpload(file);
});
dropZone?.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone?.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone?.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500');
const file = e.dataTransfer?.files[0];
if (file) handleFileUpload(file);
});
processBtn?.addEventListener('click', processAndDownloadForm);
backBtn?.addEventListener('click', () => {
window.location.href = '../../index.html';
});
});

View File

@@ -1,106 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
let viewerIframe: HTMLIFrameElement | null = null;
let viewerReady = false;
export async function setupFormFiller() {
if (!state.files || !state.files[0]) return;
showLoader('Loading PDF form...');
const pdfViewerContainer = document.getElementById('pdf-viewer-container');
if (!pdfViewerContainer) {
console.error('PDF viewer container not found');
hideLoader();
return;
}
try {
pdfViewerContainer.innerHTML = '';
const file = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file);
const blob = new Blob([arrayBuffer as ArrayBuffer], { type: 'application/pdf' });
const blobUrl = URL.createObjectURL(blob);
viewerIframe = document.createElement('iframe');
viewerIframe.src = `/pdfjs-viewer/viewer.html?file=${encodeURIComponent(blobUrl)}`;
viewerIframe.style.width = '100%';
viewerIframe.style.height = '100%';
viewerIframe.style.border = 'none';
viewerIframe.onload = () => {
viewerReady = true;
hideLoader();
};
pdfViewerContainer.appendChild(viewerIframe);
const formFillerOptions = document.getElementById('form-filler-options');
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
} catch (e) {
console.error('Critical error setting up form filler:', e);
showAlert(
'Error',
'Failed to load PDF form viewer.'
);
hideLoader();
}
}
export async function processAndDownloadForm() {
if (!viewerIframe || !viewerReady) {
showAlert('Viewer not ready', 'Please wait for the form to finish loading.');
return;
}
try {
const viewerWindow = viewerIframe.contentWindow;
if (!viewerWindow) {
console.error('Cannot access iframe window');
showAlert(
'Download',
'Please use the Download button in the PDF viewer toolbar above.'
);
return;
}
const viewerDoc = viewerWindow.document;
if (!viewerDoc) {
console.error('Cannot access iframe document');
showAlert(
'Download',
'Please use the Download button in the PDF viewer toolbar above.'
);
return;
}
const downloadBtn = viewerDoc.getElementById('downloadButton') as HTMLButtonElement | null;
if (downloadBtn) {
console.log('Clicking download button...');
downloadBtn.click();
} else {
console.error('Download button not found in viewer');
const secondaryDownload = viewerDoc.getElementById('secondaryDownload') as HTMLButtonElement | null;
if (secondaryDownload) {
console.log('Clicking secondary download button...');
secondaryDownload.click();
} else {
showAlert(
'Download',
'Please use the Download button in the PDF viewer toolbar above.'
);
}
}
} catch (e) {
console.error('Failed to trigger download:', e);
showAlert(
'Download',
'Cannot access viewer controls. Please use the Download button in the PDF viewer toolbar above.'
);
}
}

View File

@@ -0,0 +1,132 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, hexToRgb, formatBytes, parsePageRanges } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
interface PageState { file: File | null; pdfDoc: PDFLibDocument | null; }
const pageState: PageState = { file: null, pdfDoc: null };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else { initializePage(); }
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; });
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault(); dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
});
}
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
if (processBtn) processBtn.addEventListener('click', addHeaderFooter);
}
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); }
async function handleFiles(files: FileList) {
const file = files[0];
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
showLoader('Loading PDF...');
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
pageState.file = file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
const totalPagesSpan = document.getElementById('total-pages');
if (totalPagesSpan) totalPagesSpan.textContent = String(pageState.pdfDoc.getPageCount());
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
finally { hideLoader(); }
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.pdfDoc.getPageCount()} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function resetState() {
pageState.file = null; pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function addHeaderFooter() {
if (!pageState.pdfDoc) { showAlert('Error', 'Please upload a PDF file first.'); return; }
showLoader('Adding header & footer...');
try {
const helveticaFont = await pageState.pdfDoc.embedFont(StandardFonts.Helvetica);
const allPages = pageState.pdfDoc.getPages();
const totalPages = allPages.length;
const margin = 40;
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement)?.value || '10') || 10;
const colorHex = (document.getElementById('font-color') as HTMLInputElement)?.value || '#000000';
const fontColor = hexToRgb(colorHex);
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement)?.value || '';
const texts = {
headerLeft: (document.getElementById('header-left') as HTMLInputElement)?.value || '',
headerCenter: (document.getElementById('header-center') as HTMLInputElement)?.value || '',
headerRight: (document.getElementById('header-right') as HTMLInputElement)?.value || '',
footerLeft: (document.getElementById('footer-left') as HTMLInputElement)?.value || '',
footerCenter: (document.getElementById('footer-center') as HTMLInputElement)?.value || '',
footerRight: (document.getElementById('footer-right') as HTMLInputElement)?.value || '',
};
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (indicesToProcess.length === 0) throw new Error("Invalid page range specified.");
const drawOptions = { font: helveticaFont, size: fontSize, color: rgb(fontColor.r, fontColor.g, fontColor.b) };
for (const pageIndex of indicesToProcess) {
const page = allPages[pageIndex];
const { width, height } = page.getSize();
const pageNumber = pageIndex + 1;
const processText = (text: string) => text.replace(/{page}/g, String(pageNumber)).replace(/{total}/g, String(totalPages));
const processed = {
headerLeft: processText(texts.headerLeft), headerCenter: processText(texts.headerCenter), headerRight: processText(texts.headerRight),
footerLeft: processText(texts.footerLeft), footerCenter: processText(texts.footerCenter), footerRight: processText(texts.footerRight),
};
if (processed.headerLeft) page.drawText(processed.headerLeft, { ...drawOptions, x: margin, y: height - margin });
if (processed.headerCenter) page.drawText(processed.headerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.headerCenter, fontSize) / 2, y: height - margin });
if (processed.headerRight) page.drawText(processed.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.headerRight, fontSize), y: height - margin });
if (processed.footerLeft) page.drawText(processed.footerLeft, { ...drawOptions, x: margin, y: margin });
if (processed.footerCenter) page.drawText(processed.footerCenter, { ...drawOptions, x: width / 2 - helveticaFont.widthOfTextAtSize(processed.footerCenter, fontSize) / 2, y: margin });
if (processed.footerRight) page.drawText(processed.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processed.footerRight, fontSize), y: margin });
}
const newPdfBytes = await pageState.pdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'header-footer-added.pdf');
showAlert('Success', 'Header & Footer added successfully!', 'success', () => { resetState(); });
} catch (e: any) { console.error(e); showAlert('Error', e.message || 'Could not add header or footer.'); }
finally { hideLoader(); }
}

View File

@@ -0,0 +1,179 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import heic2any from 'heic2any';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
let files: File[] = [];
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const processBtn = document.getElementById('process-btn');
if (!fileDisplayArea || !fileControls || !processBtn) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
fileControls.classList.remove('hidden');
processBtn.classList.remove('hidden');
files.forEach((file, index) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-gray-200';
nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
infoContainer.append(nameSpan, sizeSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
fileControls.classList.add('hidden');
processBtn.classList.add('hidden');
}
};
const resetState = () => {
files = [];
updateUI();
};
async function convert() {
if (files.length === 0) {
showAlert('No Files', 'Please select at least one HEIC file.');
return;
}
showLoader('Converting HEIC to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of files) {
const conversionResult = await heic2any({
blob: file,
toType: 'image/png',
quality: 0.92,
});
const pngBlob = Array.isArray(conversionResult)
? conversionResult[0]
: conversionResult;
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_heic.pdf'
);
showAlert('Success', 'PDF created successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert HEIC to PDF. One of the files may be invalid.'
);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.name.toLowerCase().endsWith('.heic') || file.name.toLowerCase().endsWith('.heif')
);
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped. Only HEIC/HEIF files are allowed.');
}
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
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');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput?.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
resetState();
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -1,48 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import heic2any from 'heic2any';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function heicToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one HEIC file.');
return;
}
showLoader('Converting HEIC to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const conversionResult = await heic2any({
blob: file,
toType: 'image/png',
});
const pngBlob = Array.isArray(conversionResult)
? conversionResult[0]
: conversionResult;
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_heic.pdf'
);
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.'
);
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,321 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
let files: File[] = [];
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const addMoreBtn = document.getElementById('add-more-btn');
const clearFilesBtn = document.getElementById('clear-files-btn');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) {
handleFiles(droppedFiles);
}
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput?.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', () => {
files = [];
updateUI();
});
}
if (processBtn) {
processBtn.addEventListener('click', convertToPdf);
}
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
handleFiles(input.files);
}
}
function handleFiles(newFiles: FileList) {
const validFiles = Array.from(newFiles).filter(file =>
file.type.startsWith('image/')
);
if (validFiles.length < newFiles.length) {
showAlert('Invalid Files', 'Some files were skipped. Only image files are allowed.');
}
if (validFiles.length > 0) {
files = [...files, ...validFiles];
updateUI();
}
}
const resetState = () => {
files = [];
updateUI();
};
function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const fileControls = document.getElementById('file-controls');
const optionsDiv = document.getElementById('jpg-to-pdf-options');
if (!fileDisplayArea || !fileControls || !optionsDiv) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
fileControls.classList.remove('hidden');
optionsDiv.classList.remove('hidden');
files.forEach((file, index) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex items-center gap-2 overflow-hidden';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-gray-200';
nameSpan.textContent = file.name;
const sizeSpan = document.createElement('span');
sizeSpan.className = 'flex-shrink-0 text-gray-400 text-xs';
sizeSpan.textContent = `(${formatBytes(file.size)})`;
infoContainer.append(nameSpan, sizeSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = files.filter((_, i) => i !== index);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
} else {
fileControls.classList.add('hidden');
optionsDiv.classList.add('hidden');
}
}
function sanitizeImageAsJpeg(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(imageUrl);
return reject(new Error('Could not get canvas context'));
}
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (jpegBlob) => {
if (!jpegBlob) {
return reject(new Error('Canvas toBlob conversion failed.'));
}
const arrayBuffer = await jpegBlob.arrayBuffer();
resolve(new Uint8Array(arrayBuffer));
},
'image/jpeg',
0.9
);
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(
new Error(
'The provided file could not be loaded as an image. It may be corrupted.'
)
);
};
img.src = imageUrl;
});
}
// Special handler for SVG files - must read as text
function svgToPng(svgText: string): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
const img = new Image();
const svgBlob = new Blob([svgText], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);
img.onload = () => {
const canvas = document.createElement('canvas');
const width = img.naturalWidth || img.width || 800;
const height = img.naturalHeight || img.height || 600;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(url);
return reject(new Error('Could not get canvas context'));
}
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
async (pngBlob) => {
URL.revokeObjectURL(url);
if (!pngBlob) {
return reject(new Error('Canvas toBlob conversion failed.'));
}
const arrayBuffer = await pngBlob.arrayBuffer();
resolve(new Uint8Array(arrayBuffer));
},
'image/png'
);
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load SVG image'));
};
img.src = url;
});
}
async function convertToPdf() {
if (files.length === 0) {
showAlert('No Files', 'Please select at least one image file.');
return;
}
showLoader('Creating PDF from images...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of files) {
try {
const isSvg = file.type === 'image/svg+xml' || file.name.toLowerCase().endsWith('.svg');
if (isSvg) {
// Handle SVG files - read as text
const svgText = await file.text();
const pngBytes = await svgToPng(svgText);
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
} else if (file.type === 'image/png') {
// Handle PNG files
const originalBytes = await readFileAsArrayBuffer(file);
const pngImage = await pdfDoc.embedPng(originalBytes as Uint8Array);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
} else {
// Handle JPG/other raster images
const originalBytes = await readFileAsArrayBuffer(file);
let jpgImage;
try {
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
} catch (e) {
// Fallback: convert to JPEG via canvas
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
}
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
page.drawImage(jpgImage, {
x: 0,
y: 0,
width: jpgImage.width,
height: jpgImage.height,
});
}
} catch (error) {
console.error(`Failed to process ${file.name}:`, error);
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
}
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_images.pdf'
);
showAlert('Success', 'PDF created successfully!', 'success', () => {
resetState();
});
} catch (e: any) {
console.error(e);
showAlert('Conversion Error', e.message);
} finally {
hideLoader();
}
}

View File

@@ -1,168 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { jpgToPdf } from './jpg-to-pdf.js';
import { pngToPdf } from './png-to-pdf.js';
import { webpToPdf } from './webp-to-pdf.js';
import { bmpToPdf } from './bmp-to-pdf.js';
import { tiffToPdf } from './tiff-to-pdf.js';
import { svgToPdf } from './svg-to-pdf.js';
import { heicToPdf } from './heic-to-pdf.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function imageToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one image file.');
return;
}
const filesByType: { [key: string]: File[] } = {};
for (const file of state.files) {
const type = file.type || '';
if (!filesByType[type]) {
filesByType[type] = [];
}
filesByType[type].push(file);
}
const types = Object.keys(filesByType);
if (types.length === 1) {
const type = types[0];
const originalFiles = state.files;
if (type === 'image/jpeg' || type === 'image/jpg') {
state.files = filesByType[type] as File[];
await jpgToPdf();
} else if (type === 'image/png') {
state.files = filesByType[type] as File[];
await pngToPdf();
} else if (type === 'image/webp') {
state.files = filesByType[type] as File[];
await webpToPdf();
} else if (type === 'image/bmp') {
state.files = filesByType[type] as File[];
await bmpToPdf();
} else if (type === 'image/tiff' || type === 'image/tif') {
state.files = filesByType[type] as File[];
await tiffToPdf();
} else if (type === 'image/svg+xml') {
state.files = filesByType[type] as File[];
await svgToPdf();
} else {
const firstFile = filesByType[type][0];
if (firstFile.name.toLowerCase().endsWith('.heic') ||
firstFile.name.toLowerCase().endsWith('.heif')) {
state.files = filesByType[type] as File[];
await heicToPdf();
} else {
showLoader('Converting images to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of filesByType[type]) {
const imageBitmap = await createImageBitmap(file);
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
const pngBlob = await new Promise<Blob>((resolve) =>
canvas.toBlob(resolve, 'image/png')
);
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
imageBitmap.close();
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from-images.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert images to PDF.');
} finally {
hideLoader();
}
}
}
state.files = originalFiles;
return;
}
showLoader('Converting mixed image types to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
const imageList = document.getElementById('image-list');
const sortedFiles = imageList
? Array.from(imageList.children)
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
.map((li) => state.files.find((f) => f.name === li.dataset.fileName))
.filter(Boolean)
: state.files;
const qualityInput = document.getElementById('image-pdf-quality') as HTMLInputElement;
const quality = qualityInput ? Math.max(0.3, Math.min(1.0, parseFloat(qualityInput.value))) : 0.9;
for (const file of sortedFiles) {
const type = file.type || '';
let image;
try {
const imageBitmap = await createImageBitmap(file);
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
const jpegBlob = await new Promise<Blob>((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', quality)
);
const jpegBytes = new Uint8Array(await jpegBlob.arrayBuffer());
image = await pdfDoc.embedJpg(jpegBytes);
imageBitmap.close();
const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
} catch (e) {
console.warn(`Failed to process ${file.name}:`, e);
// Continue with next file
}
}
if (pdfDoc.getPageCount() === 0) {
throw new Error(
'No valid images could be processed. Please check your files.'
);
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from-images.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Failed to create PDF from images.');
} finally {
hideLoader();
}
}

View File

@@ -1,150 +1,81 @@
import { encrypt } from './encrypt.js';
import { decrypt } from './decrypt.js';
import { organize } from './organize.js';
import { rotate } from './rotate.js';
import { addPageNumbers } from './add-page-numbers.js';
import { pdfToJpg } from './pdf-to-jpg.js';
import { jpgToPdf } from './jpg-to-pdf.js';
import { scanToPdf } from './scan-to-pdf.js';
import { pdfToGreyscale } from './pdf-to-greyscale.js';
import { pdfToZip } from './pdf-to-zip.js';
import { editMetadata } from './edit-metadata.js';
import { removeMetadata } from './remove-metadata.js';
import { flatten } from './flatten.js';
import { pdfToPng } from './pdf-to-png.js';
import { pngToPdf } from './png-to-pdf.js';
import { pdfToWebp } from './pdf-to-webp.js';
import { webpToPdf } from './webp-to-pdf.js';
import { deletePages, setupDeletePagesTool } from './delete-pages.js';
import { addBlankPage } from './add-blank-page.js';
import { extractPages } from './extract-pages.js';
import { addWatermark, setupWatermarkUI } from './add-watermark.js';
import { addHeaderFooter } from './add-header-footer.js';
import { imageToPdf } from './image-to-pdf.js';
import { changePermissions } from './change-permissions.js';
import { pdfToMarkdown } from './pdf-to-markdown.js';
import { txtToPdf, setupTxtToPdfTool } from './txt-to-pdf.js';
import { invertColors } from './invert-colors.js';
// import { viewMetadata } from './view-metadata.js';
import { reversePages } from './reverse-pages.js';
// import { mdToPdf } from './md-to-pdf.js';
import { svgToPdf } from './svg-to-pdf.js';
import { bmpToPdf } from './bmp-to-pdf.js';
import { heicToPdf } from './heic-to-pdf.js';
import { tiffToPdf } from './tiff-to-pdf.js';
import { pdfToBmp } from './pdf-to-bmp.js';
import { pdfToTiff } from './pdf-to-tiff.js';
import { splitInHalf } from './split-in-half.js';
import { analyzeAndDisplayDimensions } from './page-dimensions.js';
import { nUpTool, setupNUpUI } from './n-up.js';
import { processAndSave } from './duplicate-organize.js';
import { combineToSinglePage } from './combine-single-page.js';
import { fixDimensions, setupFixDimensionsUI } from './fix-dimensions.js';
import { changeBackgroundColor } from './change-background-color.js';
import { changeTextColor, setupTextColorTool } from './change-text-color.js';
import { setupCompareTool } from './compare-pdfs.js';
import { setupOcrTool } from './ocr-pdf.js';
import { wordToPdf } from './word-to-pdf.js';
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
import {
removeAnnotations,
setupRemoveAnnotationsTool,
} from './remove-annotations.js';
import { setupCropperTool } from './cropper.js';
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
import { posterize, setupPosterizeTool } from './posterize.js';
import {
removeBlankPages,
setupRemoveBlankPagesTool,
} from './remove-blank-pages.js';
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
import { linearizePdf } from './linearize.js';
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
import { extractAttachments } from './extract-attachments.js';
import { editAttachments, setupEditAttachmentsTool } from './edit-attachments.js';
import { sanitizePdf } from './sanitize-pdf.js';
import { removeRestrictions } from './remove-restrictions.js';
import { repairPdf } from './repair-pdf.js';
// import { mdToPdf } from './md-to-pdf.js';
import { processAndSave } from './duplicate-organize.js';
import { wordToPdf } from './word-to-pdf.js';
import { setupCropperTool } from './cropper.js';
export const toolLogic = {
encrypt,
decrypt,
'remove-restrictions': removeRestrictions,
'repair-pdf': repairPdf,
organize,
rotate,
'add-page-numbers': addPageNumbers,
'pdf-to-jpg': pdfToJpg,
'jpg-to-pdf': jpgToPdf,
'scan-to-pdf': scanToPdf,
'pdf-to-greyscale': pdfToGreyscale,
'pdf-to-zip': pdfToZip,
'edit-metadata': editMetadata,
'remove-metadata': removeMetadata,
flatten,
'pdf-to-png': pdfToPng,
'png-to-pdf': pngToPdf,
'pdf-to-webp': pdfToWebp,
'webp-to-pdf': webpToPdf,
'delete-pages': { process: deletePages, setup: setupDeletePagesTool },
'add-blank-page': addBlankPage,
'extract-pages': extractPages,
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
'add-header-footer': addHeaderFooter,
'image-to-pdf': imageToPdf,
'change-permissions': changePermissions,
'pdf-to-markdown': pdfToMarkdown,
'txt-to-pdf': { process: txtToPdf, setup: setupTxtToPdfTool },
'invert-colors': invertColors,
'reverse-pages': reversePages,
// 'md-to-pdf': mdToPdf,
'svg-to-pdf': svgToPdf,
'bmp-to-pdf': bmpToPdf,
'heic-to-pdf': heicToPdf,
'tiff-to-pdf': tiffToPdf,
'pdf-to-bmp': pdfToBmp,
'pdf-to-tiff': pdfToTiff,
'split-in-half': splitInHalf,
'page-dimensions': analyzeAndDisplayDimensions,
'n-up': { process: nUpTool, setup: setupNUpUI },
'duplicate-organize': { process: processAndSave },
'combine-single-page': combineToSinglePage,
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
'change-background-color': changeBackgroundColor,
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
'compare-pdfs': { setup: setupCompareTool },
'ocr-pdf': { setup: setupOcrTool },
'word-to-pdf': wordToPdf,
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
'remove-annotations': {
process: removeAnnotations,
setup: setupRemoveAnnotationsTool,
},
cropper: { setup: setupCropperTool },
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller },
posterize: { process: posterize, setup: setupPosterizeTool },
'remove-blank-pages': {
process: removeBlankPages,
setup: setupRemoveBlankPagesTool,
},
'alternate-merge': {
process: alternateMerge,
setup: setupAlternateMergeTool,
},
linearize: linearizePdf,
'add-attachments': {
process: addAttachments,
setup: setupAddAttachmentsTool,
},
'extract-attachments': extractAttachments,
'edit-attachments': {
process: editAttachments,
setup: setupEditAttachmentsTool,
},
'sanitize-pdf': sanitizePdf,
};

View File

@@ -0,0 +1,131 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface PageState { file: File | null; pdfDoc: PDFLibDocument | null; }
const pageState: PageState = { file: null, pdfDoc: null };
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializePage); }
else { initializePage(); }
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; });
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-indigo-500'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-indigo-500'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault(); dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) handleFiles(e.dataTransfer.files);
});
}
if (backBtn) backBtn.addEventListener('click', () => { window.location.href = import.meta.env.BASE_URL; });
if (processBtn) processBtn.addEventListener('click', invertColors);
}
function handleFileUpload(e: Event) { const input = e.target as HTMLInputElement; if (input.files?.length) handleFiles(input.files); }
async function handleFiles(files: FileList) {
const file = files[0];
if (!file || file.type !== 'application/pdf') { showAlert('Invalid File', 'Please upload a valid PDF file.'); return; }
showLoader('Loading PDF...');
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
pageState.file = file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) { console.error(error); showAlert('Error', 'Failed to load PDF file.'); }
finally { hideLoader(); }
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.pdfDoc.getPageCount()} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function resetState() {
pageState.file = null; pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function invertColors() {
if (!pageState.pdfDoc || !pageState.file) { showAlert('Error', 'Please upload a PDF file first.'); return; }
showLoader('Inverting PDF colors...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await pageState.pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
showLoader(`Processing page ${i} of ${pdfjsDoc.numPages}...`);
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d')!;
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
data[j] = 255 - data[j];
data[j + 1] = 255 - data[j + 1];
data[j + 2] = 255 - data[j + 2];
}
ctx.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise<Uint8Array>((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
reader.readAsArrayBuffer(blob!);
}, 'image/png')
);
const image = await newPdfDoc.embedPng(pngImageBytes);
const newPage = newPdfDoc.addPage([image.width, image.height]);
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'inverted.pdf');
showAlert('Success', 'Colors inverted successfully!', 'success', () => { resetState(); });
} catch (e) { console.error(e); showAlert('Error', 'Could not invert PDF colors.'); }
finally { hideLoader(); }
}

View File

@@ -1,68 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function invertColors() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
showLoader('Inverting PDF colors...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
data[j] = 255 - data[j]; // red
data[j + 1] = 255 - data[j + 1]; // green
data[j + 2] = 255 - data[j + 2]; // blue
}
ctx.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png')
);
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([image.width, image.height]);
newPage.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'inverted.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not invert PDF colors.');
} finally {
hideLoader();
}
}

View File

@@ -43,8 +43,9 @@ function initializePage() {
}
});
dropZone.addEventListener('click', () => {
fileInput?.click();
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
@@ -75,7 +76,6 @@ function handleFileUpload(e: Event) {
if (input.files && input.files.length > 0) {
handleFiles(input.files);
}
input.value = '';
}
function handleFiles(newFiles: FileList) {

View File

@@ -1,107 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
/**
* Takes any image byte array and uses the browser's canvas to convert it
* into a standard, web-friendly (baseline, sRGB) JPEG byte array.
* @param {Uint8Array} imageBytes The raw bytes of the image file.
* @returns {Promise<Uint8Array>} A promise that resolves with the sanitized JPEG bytes.
*/
function sanitizeImageAsJpeg(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (jpegBlob) => {
if (!jpegBlob) {
return reject(new Error('Canvas toBlob conversion failed.'));
}
const arrayBuffer = await jpegBlob.arrayBuffer();
resolve(new Uint8Array(arrayBuffer));
},
'image/jpeg',
0.9
);
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(
new Error(
'The provided file could not be loaded as an image. It may be corrupted.'
)
);
};
img.src = imageUrl;
});
}
export async function jpgToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one JPG file.');
return;
}
showLoader('Creating PDF from JPGs...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const originalBytes = await readFileAsArrayBuffer(file);
let jpgImage;
try {
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
} catch (e) {
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
showAlert(
`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`
);
try {
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
} catch (fallbackError) {
console.error(
`Failed to process ${file.name} after sanitization:`,
fallbackError
);
throw new Error(
`Could not process "${file.name}". The file may be corrupted.`
);
}
}
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
page.drawImage(jpgImage, {
x: 0,
y: 0,
width: jpgImage.width,
height: jpgImage.height,
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
'from_jpgs.pdf'
);
} catch (e) {
console.error(e);
showAlert('Conversion Error', e.message);
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,240 @@
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, initializeQpdf, readFileAsArrayBuffer } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
import JSZip from 'jszip';
interface PageState {
files: File[];
}
const pageState: PageState = {
files: [],
};
function resetState() {
pageState.files = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileControls = document.getElementById('file-controls');
if (fileControls) fileControls.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
const fileControls = document.getElementById('file-controls');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.files.length > 0) {
pageState.files.forEach((file, index) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
pageState.files.splice(index, 1);
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
});
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
if (fileControls) fileControls.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
if (fileControls) fileControls.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
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) {
pageState.files.push(...pdfFiles);
updateUI();
}
}
}
async function linearizePdf() {
const pdfFiles = pageState.files.filter(
(file: File) => file.type === 'application/pdf'
);
if (!pdfFiles || pdfFiles.length === 0) {
showAlert('No PDF Files', 'Please upload at least one PDF file.');
return;
}
const loaderModal = document.getElementById('loader-modal');
const loaderText = document.getElementById('loader-text');
if (loaderModal) loaderModal.classList.remove('hidden');
if (loaderText) loaderText.textContent = 'Optimizing PDFs for web view (linearizing)...';
const zip = new JSZip();
let qpdf: any;
let successCount = 0;
let errorCount = 0;
try {
qpdf = await initializeQpdf();
for (let i = 0; i < pdfFiles.length; i++) {
const file = pdfFiles[i];
const inputPath = `/input_${i}.pdf`;
const outputPath = `/output_${i}.pdf`;
if (loaderText) loaderText.textContent = `Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`;
try {
const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
const args = [inputPath, '--linearize', outputPath];
qpdf.callMain(args);
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (!outputFile || outputFile.length === 0) {
console.error(
`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 });
successCount++;
} catch (fileError: any) {
errorCount++;
console.error(`Failed to linearize ${file.name}:`, fileError);
} finally {
try {
if (qpdf?.FS) {
if (qpdf.FS.analyzePath(inputPath).exists) {
qpdf.FS.unlink(inputPath);
}
if (qpdf.FS.analyzePath(outputPath).exists) {
qpdf.FS.unlink(outputPath);
}
}
} catch (cleanupError) {
console.warn(
`Failed to cleanup WASM FS for ${file.name}:`,
cleanupError
);
}
}
}
if (successCount === 0) {
throw new Error('No PDF files could be linearized.');
}
if (loaderText) loaderText.textContent = 'Generating ZIP file...';
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'linearized-pdfs.zip');
let alertMessage = `${successCount} PDF(s) linearized successfully.`;
if (errorCount > 0) {
alertMessage += ` ${errorCount} file(s) failed.`;
}
showAlert('Processing Complete', alertMessage, 'success', () => { resetState(); });
} catch (error: any) {
console.error('Linearization process error:', error);
showAlert(
'Linearization Failed',
`An error occurred: ${error.message || 'Unknown error'}.`
);
} finally {
if (loaderModal) loaderModal.classList.add('hidden');
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
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', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', linearizePdf);
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', function () {
fileInput.value = '';
fileInput.click();
});
}
if (clearFilesBtn) {
clearFilesBtn.addEventListener('click', function () {
resetState();
});
}
});

View File

@@ -1,123 +0,0 @@
import createModule from '@neslinesli93/qpdf-wasm';
import { showLoader, hideLoader, showAlert } from '../ui';
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
import { state } from '../state';
import JSZip from 'jszip';
let qpdfInstance: any = null;
async function initializeQpdf() {
if (qpdfInstance) {
return qpdfInstance;
}
showLoader('Initializing optimization engine...');
try {
qpdfInstance = await createModule({
locateFile: () => '/qpdf.wasm',
});
} catch (error) {
console.error('Failed to initialize qpdf-wasm:', error);
showAlert(
'Initialization Error',
'Could not load the optimization engine. Please refresh the page and try again.'
);
throw error;
} finally {
hideLoader();
}
return qpdfInstance;
}
export async function linearizePdf() {
// Check if there are files and at least one PDF
const pdfFiles = state.files.filter(
(file: File) => file.type === 'application/pdf'
);
if (!pdfFiles || pdfFiles.length === 0) {
showAlert('No PDF Files', 'Please upload at least one PDF file.');
return;
}
showLoader('Optimizing PDFs for web view (linearizing)...');
const zip = new JSZip(); // Create a JSZip instance
let qpdf: any;
let successCount = 0;
let errorCount = 0;
try {
qpdf = await initializeQpdf();
for (let i = 0; i < pdfFiles.length; i++) {
const file = pdfFiles[i];
const inputPath = `/input_${i}.pdf`;
const outputPath = `/output_${i}.pdf`;
showLoader(`Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`);
try {
const fileBuffer = await readFileAsArrayBuffer(file);
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
qpdf.FS.writeFile(inputPath, uint8Array);
const args = [inputPath, '--linearize', outputPath];
qpdf.callMain(args);
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
if (!outputFile || outputFile.length === 0) {
console.error(
`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 });
successCount++;
} catch (fileError: any) {
errorCount++;
console.error(`Failed to linearize ${file.name}:`, fileError);
// Optionally add an error marker/file to the zip? For now, we just skip.
} finally {
// Clean up WASM filesystem for this file
try {
if (qpdf?.FS) {
if (qpdf.FS.analyzePath(inputPath).exists) {
qpdf.FS.unlink(inputPath);
}
if (qpdf.FS.analyzePath(outputPath).exists) {
qpdf.FS.unlink(outputPath);
}
}
} catch (cleanupError) {
console.warn(
`Failed to cleanup WASM FS for ${file.name}:`,
cleanupError
);
}
}
}
if (successCount === 0) {
throw new Error('No PDF files could be linearized.');
}
showLoader('Generating ZIP file...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'linearized-pdfs.zip');
let alertMessage = `${successCount} PDF(s) linearized successfully.`;
if (errorCount > 0) {
alertMessage += ` ${errorCount} file(s) failed.`;
}
showAlert('Processing Complete', alertMessage);
} catch (error: any) {
console.error('Linearization process error:', error);
showAlert(
'Linearization Failed',
`An error occurred: ${error.message || 'Unknown error'}.`
);
} finally {
hideLoader();
}
}

View File

@@ -435,7 +435,7 @@ export async function refreshMergeUI() {
if (!fileModeBtn || !pageModeBtn || !filePanel || !pagePanel || !fileList) return;
fileList.textContent = ''; // Clear list safely
(state.files as File[]).forEach((f) => {
(state.files as File[]).forEach((f, index) => {
const doc = mergeState.pdfDocs[f.name];
const pageCount = doc ? doc.numPages : 'N/A';
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
@@ -461,7 +461,10 @@ export async function refreshMergeUI() {
mainDiv.append(nameSpan, dragHandle);
const rangeDiv = document.createElement('div');
rangeDiv.className = 'mt-2';
rangeDiv.className = 'mt-2 flex items-center gap-2';
const inputWrapper = document.createElement('div');
inputWrapper.className = 'flex-1';
const label = document.createElement('label');
label.htmlFor = `range-${safeFileName}`;
@@ -475,11 +478,24 @@ export async function refreshMergeUI() {
'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
input.placeholder = 'Leave blank for all pages';
rangeDiv.append(label, input);
inputWrapper.append(label, input);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 self-end';
deleteBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
deleteBtn.title = 'Remove file';
deleteBtn.onclick = (e) => {
e.stopPropagation();
state.files = state.files.filter((_, i) => i !== index);
updateUI();
};
rangeDiv.append(inputWrapper, deleteBtn);
li.append(mainDiv, rangeDiv);
fileList.appendChild(li);
});
createIcons({ icons });
initializeFileListSortable();
const newFileModeBtn = fileModeBtn.cloneNode(true) as HTMLElement;
@@ -558,7 +574,6 @@ document.addEventListener('DOMContentLoaded', () => {
state.files = [...state.files, ...Array.from(files)];
await updateUI();
}
fileInput.value = '';
});
dropZone.addEventListener('dragover', (e) => {
@@ -584,13 +599,15 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
dropZone.addEventListener('click', () => {
fileInput.click();
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => {
fileInput.value = '';
fileInput.click();
});
}

View File

@@ -0,0 +1,266 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, hexToRgb } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
interface NUpState {
file: File | null;
pdfDoc: PDFLibDocument | null;
}
const pageState: NUpState = {
file: null,
pdfDoc: null,
};
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)} • Loading...`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
try {
showLoader('Loading PDF...');
const arrayBuffer = await pageState.file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer, {
ignoreEncryption: true,
throwOnInvalidObject: false
});
hideLoader();
const pageCount = pageState.pdfDoc.getPageCount();
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageCount} pages`;
if (toolOptions) toolOptions.classList.remove('hidden');
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
resetState();
}
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
async function nUpTool() {
if (!pageState.pdfDoc || !pageState.file) {
showAlert('Error', 'Please upload a PDF first.');
return;
}
const n = parseInt((document.getElementById('pages-per-sheet') as HTMLSelectElement).value);
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value as keyof typeof PageSizes;
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
const useMargins = (document.getElementById('add-margins') as HTMLInputElement).checked;
const addBorder = (document.getElementById('add-border') as HTMLInputElement).checked;
const borderColor = hexToRgb((document.getElementById('border-color') as HTMLInputElement).value);
showLoader('Creating N-Up PDF...');
try {
const sourceDoc = pageState.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
const gridDims: Record<number, [number, number]> = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] };
const dims = gridDims[n];
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
if (orientation === 'auto') {
const firstPage = sourcePages[0];
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
orientation = isSourceLandscape && dims[0] > dims[1] ? 'landscape' : 'portrait';
}
if (orientation === 'landscape' && pageWidth < pageHeight) {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
const margin = useMargins ? 36 : 0;
const gutter = useMargins ? 10 : 0;
const usableWidth = pageWidth - margin * 2;
const usableHeight = pageHeight - margin * 2;
for (let i = 0; i < sourcePages.length; i += n) {
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
const chunk = sourcePages.slice(i, i + n);
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
const cellWidth = (usableWidth - gutter * (dims[0] - 1)) / dims[0];
const cellHeight = (usableHeight - gutter * (dims[1] - 1)) / dims[1];
for (let j = 0; j < chunk.length; j++) {
const sourcePage = chunk[j];
const embeddedPage = await newDoc.embedPage(sourcePage);
const scale = Math.min(
cellWidth / embeddedPage.width,
cellHeight / embeddedPage.height
);
const scaledWidth = embeddedPage.width * scale;
const scaledHeight = embeddedPage.height * scale;
const row = Math.floor(j / dims[0]);
const col = j % dims[0];
const cellX = margin + col * (cellWidth + gutter);
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
const x = cellX + (cellWidth - scaledWidth) / 2;
const y = cellY + (cellHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
if (addBorder) {
outputPage.drawRectangle({
x,
y,
width: scaledWidth,
height: scaledHeight,
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
borderWidth: 1,
});
}
}
}
const newPdfBytes = await newDoc.save();
const originalName = pageState.file.name.replace(/\.pdf$/i, '');
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`${originalName}_${n}-up.pdf`
);
showAlert('Success', 'N-Up PDF created successfully!', 'success', function () {
resetState();
});
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
} finally {
hideLoader();
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const addBorderCheckbox = document.getElementById('add-border');
const borderColorWrapper = document.getElementById('border-color-wrapper');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (addBorderCheckbox && borderColorWrapper) {
addBorderCheckbox.addEventListener('change', function () {
borderColorWrapper.classList.toggle('hidden', !(addBorderCheckbox as HTMLInputElement).checked);
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', nUpTool);
}
});

View File

@@ -1,128 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
export function setupNUpUI() {
const addBorderCheckbox = document.getElementById('add-border');
const borderColorWrapper = document.getElementById('border-color-wrapper');
if (addBorderCheckbox && borderColorWrapper) {
addBorderCheckbox.addEventListener('change', () => {
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
});
}
}
export async function nUpTool() {
// 1. Gather all options from the UI
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const n = parseInt(document.getElementById('pages-per-sheet').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageSizeKey = document.getElementById('output-page-size').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
let orientation = document.getElementById('output-orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const useMargins = document.getElementById('add-margins').checked;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addBorder = document.getElementById('add-border').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const borderColor = hexToRgb(document.getElementById('border-color').value);
showLoader('Creating N-Up PDF...');
try {
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
if (orientation === 'auto') {
const firstPage = sourcePages[0];
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
orientation =
isSourceLandscape && gridDims[0] > gridDims[1]
? 'landscape'
: 'portrait';
}
if (orientation === 'landscape' && pageWidth < pageHeight) {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
const margin = useMargins ? 36 : 0;
const gutter = useMargins ? 10 : 0;
const usableWidth = pageWidth - margin * 2;
const usableHeight = pageHeight - margin * 2;
// Loop through the source pages in chunks of 'n'
for (let i = 0; i < sourcePages.length; i += n) {
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
const chunk = sourcePages.slice(i, i + n);
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
// Calculate dimensions of each cell in the grid
const cellWidth =
(usableWidth - gutter * (gridDims[0] - 1)) / gridDims[0];
const cellHeight =
(usableHeight - gutter * (gridDims[1] - 1)) / gridDims[1];
for (let j = 0; j < chunk.length; j++) {
const sourcePage = chunk[j];
const embeddedPage = await newDoc.embedPage(sourcePage);
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
const scale = Math.min(
cellWidth / embeddedPage.width,
cellHeight / embeddedPage.height
);
const scaledWidth = embeddedPage.width * scale;
const scaledHeight = embeddedPage.height * scale;
// Calculate position (x, y) for this cell
const row = Math.floor(j / gridDims[0]);
const col = j % gridDims[0];
const cellX = margin + col * (cellWidth + gutter);
const cellY =
pageHeight - margin - (row + 1) * cellHeight - row * gutter;
// Center the page within its cell
const x = cellX + (cellWidth - scaledWidth) / 2;
const y = cellY + (cellHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, {
x,
y,
width: scaledWidth,
height: scaledHeight,
});
if (addBorder) {
outputPage.drawRectangle({
x,
y,
width: scaledWidth,
height: scaledHeight,
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
borderWidth: 1,
});
}
}
}
const newPdfBytes = await newDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
`n-up_${n}.pdf`
);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,567 @@
import { tesseractLanguages } from '../config/tesseract-languages.js';
import { showAlert } from '../ui.js';
import { downloadFile, formatBytes, getPDFDocument } from '../utils/helpers.js';
import Tesseract from 'tesseract.js';
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
import { getFontForLanguage } from '../utils/font-loader.js';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface Word {
text: string;
bbox: { x0: number; y0: number; x1: number; y1: number };
confidence: number;
}
interface OcrState {
file: File | null;
searchablePdfBytes: Uint8Array | null;
}
const pageState: OcrState = {
file: null,
searchablePdfBytes: null,
};
const whitelistPresets: Record<string, string> = {
alphanumeric: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"',
'numbers-currency': '0123456789$€£¥.,- ',
'letters-only': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ',
'numbers-only': '0123456789',
invoice: '0123456789$.,/-#: ',
forms: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,()-_/@#:',
};
function parseHOCR(hocrText: string): Word[] {
const parser = new DOMParser();
const doc = parser.parseFromString(hocrText, 'text/html');
const words: Word[] = [];
const wordElements = doc.querySelectorAll('.ocrx_word');
wordElements.forEach(function (wordEl) {
const titleAttr = wordEl.getAttribute('title');
const text = wordEl.textContent?.trim() || '';
if (!titleAttr || !text) return;
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
const confMatch = titleAttr.match(/x_wconf (\d+)/);
if (bboxMatch) {
words.push({
text: text,
bbox: {
x0: parseInt(bboxMatch[1]),
y0: parseInt(bboxMatch[2]),
x1: parseInt(bboxMatch[3]),
y1: parseInt(bboxMatch[4]),
},
confidence: confMatch ? parseInt(confMatch[1]) : 0,
});
}
});
return words;
}
function binarizeCanvas(ctx: CanvasRenderingContext2D) {
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const brightness = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
const color = brightness > 128 ? 255 : 0;
data[i] = data[i + 1] = data[i + 2] = color;
}
ctx.putImageData(imageData, 0, 0);
}
function updateProgress(status: string, progress: number) {
const progressBar = document.getElementById('progress-bar');
const progressStatus = document.getElementById('progress-status');
const progressLog = document.getElementById('progress-log');
if (!progressBar || !progressStatus || !progressLog) return;
progressStatus.textContent = status;
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
const logMessage = `Status: ${status}`;
progressLog.textContent += logMessage + '\n';
progressLog.scrollTop = progressLog.scrollHeight;
}
function resetState() {
pageState.file = null;
pageState.searchablePdfBytes = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const toolOptions = document.getElementById('tool-options');
if (toolOptions) toolOptions.classList.add('hidden');
const ocrProgress = document.getElementById('ocr-progress');
if (ocrProgress) ocrProgress.classList.add('hidden');
const ocrResults = document.getElementById('ocr-results');
if (ocrResults) ocrResults.classList.add('hidden');
const progressLog = document.getElementById('progress-log');
if (progressLog) progressLog.textContent = '';
const progressBar = document.getElementById('progress-bar');
if (progressBar) progressBar.style.width = '0%';
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
// Reset selected languages
const langCheckboxes = document.querySelectorAll('.lang-checkbox') as NodeListOf<HTMLInputElement>;
langCheckboxes.forEach(function (cb) { cb.checked = false; });
const selectedLangsDisplay = document.getElementById('selected-langs-display');
if (selectedLangsDisplay) selectedLangsDisplay.textContent = 'None';
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
if (processBtn) processBtn.disabled = true;
}
async function runOCR() {
const selectedLangs = Array.from(
document.querySelectorAll('.lang-checkbox:checked')
).map(function (cb) { return (cb as HTMLInputElement).value; });
const scale = parseFloat(
(document.getElementById('ocr-resolution') as HTMLSelectElement).value
);
const binarize = (document.getElementById('ocr-binarize') as HTMLInputElement).checked;
const whitelist = (document.getElementById('ocr-whitelist') as HTMLInputElement).value;
if (selectedLangs.length === 0) {
showAlert('No Languages Selected', 'Please select at least one language for OCR.');
return;
}
if (!pageState.file) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
const langString = selectedLangs.join('+');
const toolOptions = document.getElementById('tool-options');
const ocrProgress = document.getElementById('ocr-progress');
if (toolOptions) toolOptions.classList.add('hidden');
if (ocrProgress) ocrProgress.classList.remove('hidden');
try {
const worker = await Tesseract.createWorker(langString, 1, {
logger: function (m: { status: string; progress: number }) {
updateProgress(m.status, m.progress || 0);
},
});
await worker.setParameters({
tessjs_create_hocr: '1',
tessedit_pageseg_mode: Tesseract.PSM.AUTO,
});
if (whitelist) {
await worker.setParameters({
tessedit_char_whitelist: whitelist,
});
}
const arrayBuffer = await pageState.file.arrayBuffer();
const pdf = await getPDFDocument({ data: arrayBuffer }).promise;
const newPdfDoc = await PDFLibDocument.create();
newPdfDoc.registerFontkit(fontkit);
updateProgress('Loading fonts...', 0);
const cjkLangs = ['jpn', 'chi_sim', 'chi_tra', 'kor'];
const indicLangs = ['hin', 'ben', 'guj', 'kan', 'mal', 'ori', 'pan', 'tam', 'tel', 'sin'];
const priorityLangs = [...cjkLangs, ...indicLangs, 'ara', 'rus', 'ukr'];
const primaryLang = selectedLangs.find(function (l) { return priorityLangs.includes(l); }) || selectedLangs[0] || 'eng';
const hasCJK = selectedLangs.some(function (l) { return cjkLangs.includes(l); });
const hasIndic = selectedLangs.some(function (l) { return indicLangs.includes(l); });
const hasLatin = selectedLangs.some(function (l) { return !priorityLangs.includes(l); }) || selectedLangs.includes('eng');
const isIndicPlusLatin = hasIndic && hasLatin && !hasCJK;
let primaryFont;
let latinFont;
try {
if (isIndicPlusLatin) {
const [scriptFontBytes, latinFontBytes] = await Promise.all([
getFontForLanguage(primaryLang),
getFontForLanguage('eng')
]);
primaryFont = await newPdfDoc.embedFont(scriptFontBytes, { subset: false });
latinFont = await newPdfDoc.embedFont(latinFontBytes, { subset: false });
} else {
const fontBytes = await getFontForLanguage(primaryLang);
primaryFont = await newPdfDoc.embedFont(fontBytes, { subset: false });
latinFont = primaryFont;
}
} catch (e) {
console.error('Font loading failed, falling back to Helvetica', e);
primaryFont = await newPdfDoc.embedFont(StandardFonts.Helvetica);
latinFont = primaryFont;
showAlert('Font Warning', 'Could not load the specific font for this language. Some characters may not appear correctly.');
}
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
updateProgress(`Processing page ${i} of ${pdf.numPages}`, (i - 1) / pdf.numPages);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d')!;
await page.render({ canvasContext: context, viewport, canvas }).promise;
if (binarize) {
binarizeCanvas(context);
}
const result = await worker.recognize(canvas, {}, { text: true, hocr: true });
const data = result.data;
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
const pngImageBytes = await new Promise<Uint8Array>(function (resolve) {
canvas.toBlob(function (blob) {
const reader = new FileReader();
reader.onload = function () {
resolve(new Uint8Array(reader.result as ArrayBuffer));
};
reader.readAsArrayBuffer(blob!);
}, 'image/png');
});
const pngImage = await newPdfDoc.embedPng(pngImageBytes);
newPage.drawImage(pngImage, {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
});
if (data.hocr) {
const words = parseHOCR(data.hocr);
words.forEach(function (word: Word) {
const { x0, y0, x1, y1 } = word.bbox;
const text = word.text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '');
if (!text.trim()) return;
const hasNonLatin = /[^\u0000-\u007F]/.test(text);
const font = hasNonLatin ? primaryFont : latinFont;
if (!font) {
console.warn(`Font not available for text: "${text}"`);
return;
}
const bboxWidth = x1 - x0;
const bboxHeight = y1 - y0;
if (bboxWidth <= 0 || bboxHeight <= 0) {
return;
}
let fontSize = bboxHeight * 0.9;
try {
let textWidth = font.widthOfTextAtSize(text, fontSize);
while (textWidth > bboxWidth && fontSize > 1) {
fontSize -= 0.5;
textWidth = font.widthOfTextAtSize(text, fontSize);
}
} catch (error) {
console.warn(`Could not calculate text width for "${text}":`, error);
return;
}
try {
newPage.drawText(text, {
x: x0,
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
font,
size: fontSize,
color: rgb(0, 0, 0),
opacity: 0,
});
} catch (error) {
console.warn(`Could not draw text "${text}":`, error);
}
});
}
fullText += data.text + '\n\n';
}
await worker.terminate();
pageState.searchablePdfBytes = await newPdfDoc.save();
const ocrResults = document.getElementById('ocr-results');
if (ocrProgress) ocrProgress.classList.add('hidden');
if (ocrResults) ocrResults.classList.remove('hidden');
createIcons({ icons });
const textOutput = document.getElementById('ocr-text-output') as HTMLTextAreaElement;
if (textOutput) textOutput.value = fullText.trim();
} catch (e) {
console.error(e);
showAlert('OCR Error', 'An error occurred during the OCR process. The worker may have failed to load. Please try again.');
if (toolOptions) toolOptions.classList.remove('hidden');
if (ocrProgress) ocrProgress.classList.add('hidden');
}
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
const toolOptions = document.getElementById('tool-options');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
if (toolOptions) toolOptions.classList.remove('hidden');
} else {
if (toolOptions) toolOptions.classList.add('hidden');
}
}
function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
updateUI();
}
}
}
function populateLanguageList() {
const langList = document.getElementById('lang-list');
if (!langList) return;
langList.innerHTML = '';
Object.entries(tesseractLanguages).forEach(function ([code, name]) {
const label = document.createElement('label');
label.className = 'flex items-center gap-2 p-2 rounded-md hover:bg-gray-700 cursor-pointer';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = code;
checkbox.className = 'lang-checkbox w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
label.append(checkbox);
label.append(document.createTextNode(' ' + name));
langList.appendChild(label);
});
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn') as HTMLButtonElement;
const backBtn = document.getElementById('back-to-tools');
const langSearch = document.getElementById('lang-search') as HTMLInputElement;
const langList = document.getElementById('lang-list');
const selectedLangsDisplay = document.getElementById('selected-langs-display');
const presetSelect = document.getElementById('whitelist-preset') as HTMLSelectElement;
const whitelistInput = document.getElementById('ocr-whitelist') as HTMLInputElement;
const copyBtn = document.getElementById('copy-text-btn');
const downloadTxtBtn = document.getElementById('download-txt-btn');
const downloadPdfBtn = document.getElementById('download-searchable-pdf');
populateLanguageList();
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (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(function (f) {
return f.type === 'application/pdf' || f.name.toLowerCase().endsWith('.pdf');
});
if (pdfFiles.length > 0) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(pdfFiles[0]);
handleFileSelect(dataTransfer.files);
}
}
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
// Language search
if (langSearch && langList) {
langSearch.addEventListener('input', function () {
const searchTerm = langSearch.value.toLowerCase();
langList.querySelectorAll('label').forEach(function (label) {
(label as HTMLElement).style.display = label.textContent?.toLowerCase().includes(searchTerm) ? '' : 'none';
});
});
langList.addEventListener('change', function () {
const selected = Array.from(
langList.querySelectorAll('.lang-checkbox:checked')
).map(function (cb) {
return tesseractLanguages[(cb as HTMLInputElement).value as keyof typeof tesseractLanguages];
});
if (selectedLangsDisplay) {
selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None';
}
if (processBtn) {
processBtn.disabled = selected.length === 0;
}
});
}
// Whitelist preset
if (presetSelect && whitelistInput) {
presetSelect.addEventListener('change', function () {
const preset = presetSelect.value;
if (preset && preset !== 'custom') {
whitelistInput.value = whitelistPresets[preset] || '';
whitelistInput.disabled = true;
} else {
whitelistInput.disabled = false;
if (preset === '') {
whitelistInput.value = '';
}
}
});
}
// Details toggle
document.querySelectorAll('details').forEach(function (details) {
details.addEventListener('toggle', function () {
const icon = details.querySelector('.details-icon') as HTMLElement;
if (icon) {
icon.style.transform = (details as HTMLDetailsElement).open ? 'rotate(180deg)' : 'rotate(0deg)';
}
});
});
// Process button
if (processBtn) {
processBtn.addEventListener('click', runOCR);
}
// Copy button
if (copyBtn) {
copyBtn.addEventListener('click', function () {
const textOutput = document.getElementById('ocr-text-output') as HTMLTextAreaElement;
if (textOutput) {
navigator.clipboard.writeText(textOutput.value).then(function () {
copyBtn.innerHTML = '<i data-lucide="check" class="w-4 h-4 text-green-400"></i>';
createIcons({ icons });
setTimeout(function () {
copyBtn.innerHTML = '<i data-lucide="clipboard-copy" class="w-4 h-4 text-gray-300"></i>';
createIcons({ icons });
}, 2000);
});
}
});
}
// Download txt
if (downloadTxtBtn) {
downloadTxtBtn.addEventListener('click', function () {
const textOutput = document.getElementById('ocr-text-output') as HTMLTextAreaElement;
if (textOutput) {
const blob = new Blob([textOutput.value], { type: 'text/plain' });
downloadFile(blob, 'ocr-text.txt');
}
});
}
// Download PDF
if (downloadPdfBtn) {
downloadPdfBtn.addEventListener('click', function () {
if (pageState.searchablePdfBytes) {
downloadFile(
new Blob([new Uint8Array(pageState.searchablePdfBytes)], { type: 'application/pdf' }),
'searchable.pdf'
);
}
});
}
});

View File

@@ -1,421 +0,0 @@
import { tesseractLanguages } from '../config/tesseract-languages.js';
import { showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import Tesseract from 'tesseract.js';
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import { icons, createIcons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
import type { Word } from '../types/index.js';
let searchablePdfBytes: Uint8Array | null = null;
import { getFontForLanguage } from '../utils/font-loader.js';
// function sanitizeTextForWinAnsi(text: string): string {
// // Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
// return text
// .replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
// .replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
// }
function parseHOCR(hocrText: string) {
const parser = new DOMParser();
const doc = parser.parseFromString(hocrText, 'text/html');
const words = [];
// Find all word elements in hOCR
const wordElements = doc.querySelectorAll('.ocrx_word');
wordElements.forEach((wordEl) => {
const titleAttr = wordEl.getAttribute('title');
const text = wordEl.textContent?.trim() || '';
if (!titleAttr || !text) return;
// Parse bbox coordinates from title attribute
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
const confMatch = titleAttr.match(/x_wconf (\d+)/);
if (bboxMatch) {
words.push({
text: text,
bbox: {
x0: parseInt(bboxMatch[1]),
y0: parseInt(bboxMatch[2]),
x1: parseInt(bboxMatch[3]),
y1: parseInt(bboxMatch[4]),
},
confidence: confMatch ? parseInt(confMatch[1]) : 0,
});
}
});
return words;
}
function binarizeCanvas(ctx: CanvasRenderingContext2D) {
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// A simple luminance-based threshold for determining black or white
const brightness =
0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
data[i] = data[i + 1] = data[i + 2] = color;
}
ctx.putImageData(imageData, 0, 0);
}
function updateProgress(status: string, progress: number) {
const progressBar = document.getElementById('progress-bar');
const progressStatus = document.getElementById('progress-status');
const progressLog = document.getElementById('progress-log');
if (!progressBar || !progressStatus || !progressLog) return;
progressStatus.textContent = status;
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
const logMessage = `Status: ${status}`;
progressLog.textContent += logMessage + '\n';
progressLog.scrollTop = progressLog.scrollHeight;
}
async function runOCR() {
const selectedLangs = Array.from(
document.querySelectorAll('.lang-checkbox:checked')
).map((cb) => (cb as HTMLInputElement).value);
const scale = parseFloat(
(document.getElementById('ocr-resolution') as HTMLSelectElement).value
);
const binarize = (document.getElementById('ocr-binarize') as HTMLInputElement)
.checked;
const whitelist = (document.getElementById('ocr-whitelist') as HTMLInputElement)
.value;
if (selectedLangs.length === 0) {
showAlert(
'No Languages Selected',
'Please select at least one language for OCR.'
);
return;
}
const langString = selectedLangs.join('+');
document.getElementById('ocr-options').classList.add('hidden');
document.getElementById('ocr-progress').classList.remove('hidden');
try {
const worker = await Tesseract.createWorker(langString, 1, {
logger: (m: { status: string; progress: number }) =>
updateProgress(m.status, m.progress || 0),
});
await worker.setParameters({
tessjs_create_hocr: '1',
tessedit_pageseg_mode: Tesseract.PSM.AUTO,
});
await worker.setParameters({
tessedit_char_whitelist: whitelist,
});
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const newPdfDoc = await PDFLibDocument.create();
newPdfDoc.registerFontkit(fontkit);
updateProgress('Loading fonts...', 0);
// Prioritize non-Latin languages for font selection if multiple are selected
const cjkLangs = ['jpn', 'chi_sim', 'chi_tra', 'kor'];
const indicLangs = ['hin', 'ben', 'guj', 'kan', 'mal', 'ori', 'pan', 'tam', 'tel', 'sin'];
const priorityLangs = [...cjkLangs, ...indicLangs, 'ara', 'rus', 'ukr'];
const primaryLang = selectedLangs.find(l => priorityLangs.includes(l)) || selectedLangs[0] || 'eng';
const hasCJK = selectedLangs.some(l => cjkLangs.includes(l));
const hasIndic = selectedLangs.some(l => indicLangs.includes(l));
const hasLatin = selectedLangs.some(l => !priorityLangs.includes(l)) || selectedLangs.includes('eng');
const isIndicPlusLatin = hasIndic && hasLatin && !hasCJK;
let primaryFont;
let latinFont;
try {
let fontBytes;
if (isIndicPlusLatin) {
const [scriptFontBytes, latinFontBytes] = await Promise.all([
getFontForLanguage(primaryLang),
getFontForLanguage('eng')
]);
primaryFont = await newPdfDoc.embedFont(scriptFontBytes, { subset: false });
latinFont = await newPdfDoc.embedFont(latinFontBytes, { subset: false });
} else {
// For CJK or single-script, use one font
fontBytes = await getFontForLanguage(primaryLang);
primaryFont = await newPdfDoc.embedFont(fontBytes, { subset: false });
latinFont = primaryFont;
}
} catch (e) {
console.error('Font loading failed, falling back to Helvetica', e);
primaryFont = await newPdfDoc.embedFont(StandardFonts.Helvetica);
latinFont = primaryFont;
showAlert('Font Warning', 'Could not load the specific font for this language. Some characters may not appear correctly.');
}
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
updateProgress(
`Processing page ${i} of ${pdf.numPages}`,
(i - 1) / pdf.numPages
);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport, canvas }).promise;
if (binarize) {
binarizeCanvas(context);
}
const result = await worker.recognize(
canvas,
{},
{ text: true, hocr: true }
);
const data = result.data;
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
const pngImageBytes = await new Promise((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
reader.onload = () =>
resolve(new Uint8Array(reader.result as ArrayBuffer));
reader.readAsArrayBuffer(blob);
}, 'image/png')
);
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
newPage.drawImage(pngImage, {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
});
// Parse hOCR to get word-level data
if (data.hocr) {
const words = parseHOCR(data.hocr);
words.forEach((word: Word) => {
const { x0, y0, x1, y1 } = word.bbox;
const text = word.text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '');
if (!text.trim()) return;
const hasNonLatin = /[^\u0000-\u007F]/.test(text);
const font = hasNonLatin ? primaryFont : latinFont;
if (!font) {
console.warn(`Font not available for text: "${text}"`);
return;
}
const bboxWidth = x1 - x0;
const bboxHeight = y1 - y0;
if (bboxWidth <= 0 || bboxHeight <= 0) {
return;
}
let fontSize = bboxHeight * 0.9;
try {
let textWidth = font.widthOfTextAtSize(text, fontSize);
while (textWidth > bboxWidth && fontSize > 1) {
fontSize -= 0.5;
textWidth = font.widthOfTextAtSize(text, fontSize);
}
} catch (error) {
console.warn(`Could not calculate text width for "${text}":`, error);
return;
}
try {
newPage.drawText(text, {
x: x0,
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
font,
size: fontSize,
color: rgb(0, 0, 0),
opacity: 0,
});
} catch (error) {
console.warn(`Could not draw text "${text}":`, error);
}
});
}
fullText += data.text + '\n\n';
}
await worker.terminate();
searchablePdfBytes = await newPdfDoc.save();
document.getElementById('ocr-progress').classList.add('hidden');
document.getElementById('ocr-results').classList.remove('hidden');
createIcons({ icons });
(
document.getElementById('ocr-text-output') as HTMLTextAreaElement
).value = fullText.trim();
document
.getElementById('download-searchable-pdf')
.addEventListener('click', () => {
if (searchablePdfBytes) {
downloadFile(
new Blob([searchablePdfBytes as BlobPart], { type: 'application/pdf' }),
'searchable.pdf'
);
}
});
// CHANGE: The copy button logic is updated to be safer.
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
const button = e.currentTarget as HTMLButtonElement;
const textToCopy = (
document.getElementById('ocr-text-output') as HTMLTextAreaElement
).value;
navigator.clipboard.writeText(textToCopy).then(() => {
button.textContent = ''; // Clear the button safely
const icon = document.createElement('i');
icon.setAttribute('data-lucide', 'check');
icon.className = 'w-4 h-4 text-green-400';
button.appendChild(icon);
createIcons({ icons });
setTimeout(() => {
const currentButton = document.getElementById('copy-text-btn');
if (currentButton) {
currentButton.textContent = ''; // Clear the button safely
const resetIcon = document.createElement('i');
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
resetIcon.className = 'w-4 h-4 text-gray-300';
currentButton.appendChild(resetIcon);
createIcons({ icons });
}
}, 2000);
});
});
document
.getElementById('download-txt-btn')
.addEventListener('click', () => {
const textToSave = (
document.getElementById('ocr-text-output') as HTMLTextAreaElement
).value;
const blob = new Blob([textToSave], { type: 'text/plain' });
downloadFile(blob, 'ocr-text.txt');
});
} catch (e) {
console.error(e);
showAlert(
'OCR Error',
'An error occurred during the OCR process. The worker may have failed to load. Please try again.'
);
document.getElementById('ocr-options').classList.remove('hidden');
document.getElementById('ocr-progress').classList.add('hidden');
}
}
/**
* Sets up the UI and event listeners for the OCR tool.
*/
export function setupOcrTool() {
const langSearch = document.getElementById('lang-search');
const langList = document.getElementById('lang-list');
const selectedLangsDisplay = document.getElementById(
'selected-langs-display'
);
const processBtn = document.getElementById('process-btn');
// 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', () => {
const searchTerm = (langSearch as HTMLInputElement).value.toLowerCase();
langList.querySelectorAll('label').forEach((label) => {
label.style.display = label.textContent.toLowerCase().includes(searchTerm)
? ''
: 'none';
});
});
langList.addEventListener('change', () => {
const selected = Array.from(
langList.querySelectorAll('.lang-checkbox:checked')
).map((cb) => tesseractLanguages[(cb as HTMLInputElement).value]);
selectedLangsDisplay.textContent =
selected.length > 0 ? selected.join(', ') : 'None';
(processBtn as HTMLButtonElement).disabled = selected.length === 0;
});
processBtn.addEventListener('click', runOCR);
}

View File

@@ -0,0 +1,294 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { readFileAsArrayBuffer, formatBytes, downloadFile, getPDFDocument } from '../utils/helpers.js';
import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
interface OrganizeState {
file: File | null;
pdfDoc: any;
pdfJsDoc: any;
totalPages: number;
sortableInstance: any;
}
const organizeState: OrganizeState = {
file: null,
pdfDoc: null,
pdfJsDoc: null,
totalPages: 0,
sortableInstance: null,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
if (fileInput) fileInput.addEventListener('change', handleFileUpload);
if (dropZone) {
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('bg-gray-700'); });
dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('bg-gray-700'); });
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
const droppedFiles = e.dataTransfer?.files;
if (droppedFiles && droppedFiles.length > 0) handleFile(droppedFiles[0]);
});
// Clear value on click to allow re-selecting the same file
fileInput?.addEventListener('click', () => {
if (fileInput) fileInput.value = '';
});
}
if (processBtn) processBtn.addEventListener('click', saveChanges);
document.getElementById('back-to-tools')?.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files && input.files.length > 0) handleFile(input.files[0]);
}
async function handleFile(file: File) {
if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) {
showAlert('Invalid File', 'Please select a PDF file.');
return;
}
showLoader('Loading PDF...');
organizeState.file = file;
try {
const arrayBuffer = await readFileAsArrayBuffer(file);
organizeState.pdfDoc = await PDFDocument.load(arrayBuffer as ArrayBuffer, { ignoreEncryption: true, throwOnInvalidObject: false });
organizeState.pdfJsDoc = await getPDFDocument({ data: (arrayBuffer as ArrayBuffer).slice(0) }).promise;
organizeState.totalPages = organizeState.pdfDoc.getPageCount();
updateFileDisplay();
await renderThumbnails();
hideLoader();
} catch (error) {
console.error('Error loading PDF:', error);
hideLoader();
showAlert('Error', 'Failed to load PDF file.');
}
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !organizeState.file) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = organizeState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(organizeState.file.size)}${organizeState.totalPages} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => resetState();
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function renumberPages() {
const grid = document.getElementById('page-grid');
if (!grid) return;
const labels = grid.querySelectorAll('.page-number');
labels.forEach((label, index) => {
label.textContent = (index + 1).toString();
});
}
function attachEventListeners(element: HTMLElement) {
const duplicateBtn = element.querySelector('.duplicate-btn');
const deleteBtn = element.querySelector('.delete-btn');
duplicateBtn?.addEventListener('click', (e) => {
e.stopPropagation();
const clone = element.cloneNode(true) as HTMLElement;
element.after(clone);
attachEventListeners(clone);
renumberPages();
createIcons({ icons });
initializeSortable();
});
deleteBtn?.addEventListener('click', (e) => {
e.stopPropagation();
const grid = document.getElementById('page-grid');
if (grid && grid.children.length > 1) {
element.remove();
renumberPages();
initializeSortable();
} else {
showAlert('Cannot Delete', 'You cannot delete the last page of the document.');
}
});
}
async function renderThumbnails() {
const grid = document.getElementById('page-grid');
const processBtn = document.getElementById('process-btn');
if (!grid) return;
grid.innerHTML = '';
grid.classList.remove('hidden');
processBtn?.classList.remove('hidden');
for (let i = 1; i <= organizeState.totalPages; i++) {
const page = await organizeState.pdfJsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
const wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
wrapper.dataset.originalPageIndex = (i - 1).toString();
const imgContainer = document.createElement('div');
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'max-w-full max-h-full object-contain';
imgContainer.appendChild(img);
const pageLabel = document.createElement('span');
pageLabel.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
pageLabel.textContent = i.toString();
const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-4';
const duplicateBtn = document.createElement('button');
duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
duplicateBtn.title = 'Duplicate Page';
duplicateBtn.innerHTML = '<i data-lucide="copy-plus" class="w-5 h-5"></i>';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
deleteBtn.title = 'Delete Page';
deleteBtn.innerHTML = '<i data-lucide="x-circle" class="w-5 h-5"></i>';
controlsDiv.append(duplicateBtn, deleteBtn);
wrapper.append(imgContainer, pageLabel, controlsDiv);
grid.appendChild(wrapper);
attachEventListeners(wrapper);
}
createIcons({ icons });
initializeSortable();
}
function initializeSortable() {
const grid = document.getElementById('page-grid');
if (!grid) return;
if (organizeState.sortableInstance) organizeState.sortableInstance.destroy();
organizeState.sortableInstance = Sortable.create(grid, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
filter: '.duplicate-btn, .delete-btn',
preventOnFilter: true,
onStart: (evt) => {
if (evt.item) evt.item.style.opacity = '0.5';
},
onEnd: (evt) => {
if (evt.item) evt.item.style.opacity = '1';
},
});
}
async function saveChanges() {
showLoader('Building new PDF...');
try {
const grid = document.getElementById('page-grid');
if (!grid) return;
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
const finalIndices = Array.from(finalPageElements)
.map(el => parseInt((el as HTMLElement).dataset.originalPageIndex || '', 10))
.filter(index => !isNaN(index) && index >= 0);
if (finalIndices.length === 0) {
showAlert('Error', 'No valid pages to save.');
return;
}
const newPdf = await PDFDocument.create();
const copiedPages = await newPdf.copyPages(organizeState.pdfDoc, finalIndices);
copiedPages.forEach(page => newPdf.addPage(page));
const pdfBytes = await newPdf.save();
const baseName = organizeState.file?.name.replace('.pdf', '') || 'document';
downloadFile(new Blob([pdfBytes as BlobPart], { type: 'application/pdf' }), `${baseName}_organized.pdf`);
hideLoader();
showAlert('Success', 'PDF organized successfully!', 'success', () => resetState());
} catch (error) {
console.error('Error saving changes:', error);
hideLoader();
showAlert('Error', 'Failed to save changes.');
}
}
function resetState() {
if (organizeState.sortableInstance) {
organizeState.sortableInstance.destroy();
organizeState.sortableInstance = null;
}
organizeState.file = null;
organizeState.pdfDoc = null;
organizeState.pdfJsDoc = null;
organizeState.totalPages = 0;
const grid = document.getElementById('page-grid');
if (grid) {
grid.innerHTML = '';
grid.classList.add('hidden');
}
document.getElementById('process-btn')?.classList.add('hidden');
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
}

View File

@@ -1,30 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function organize() {
showLoader('Saving changes...');
try {
const newPdf = await PDFLibDocument.create();
const pageContainer = document.getElementById('page-organizer');
const pageIndices = Array.from(pageContainer.children).map((child) =>
parseInt((child as HTMLElement).dataset.pageIndex)
);
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'organized.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not save the changes.');
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,359 @@
import { showAlert } from '../ui.js';
import { formatBytes, getStandardPageName, convertPoints } from '../utils/helpers.js';
import { PDFDocument } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
interface PageState {
file: File | null;
pdfDoc: PDFDocument | null;
}
const pageState: PageState = {
file: null,
pdfDoc: null,
};
let analyzedPagesData: any[] = [];
function calculateAspectRatio(width: number, height: number): string {
const ratio = width / height;
return ratio.toFixed(3);
}
function calculateArea(width: number, height: number, unit: string): string {
const areaInPoints = width * height;
let convertedArea = 0;
let unitSuffix = '';
switch (unit) {
case 'in':
convertedArea = areaInPoints / (72 * 72);
unitSuffix = 'in²';
break;
case 'mm':
convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4);
unitSuffix = 'mm²';
break;
case 'px':
const pxPerPoint = 96 / 72;
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
unitSuffix = 'px²';
break;
default:
convertedArea = areaInPoints;
unitSuffix = 'pt²';
break;
}
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
}
function getSummaryStats() {
const totalPages = analyzedPagesData.length;
const uniqueSizes = new Map();
analyzedPagesData.forEach((pageData: any) => {
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
const label = `${pageData.standardSize} (${pageData.orientation})`;
uniqueSizes.set(key, {
count: (uniqueSizes.get(key)?.count || 0) + 1,
label: label,
width: pageData.width,
height: pageData.height
});
});
const hasMixedSizes = uniqueSizes.size > 1;
return {
totalPages,
uniqueSizesCount: uniqueSizes.size,
uniqueSizes: Array.from(uniqueSizes.values()),
hasMixedSizes
};
}
function renderSummary() {
const summaryContainer = document.getElementById('dimensions-summary');
if (!summaryContainer) return;
const stats = getSummaryStats();
let summaryHTML = `
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Total Pages</p>
<p class="text-2xl font-bold text-white">${stats.totalPages}</p>
</div>
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Unique Page Sizes</p>
<p class="text-2xl font-bold text-white">${stats.uniqueSizesCount}</p>
</div>
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Document Type</p>
<p class="text-2xl font-bold ${stats.hasMixedSizes ? 'text-yellow-400' : 'text-green-400'}">
${stats.hasMixedSizes ? 'Mixed Sizes' : 'Uniform'}
</p>
</div>
</div>
`;
if (stats.hasMixedSizes) {
summaryHTML += `
<div class="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-4 mb-4">
<div class="flex items-start gap-3">
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0"></i>
<div>
<h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4>
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
<ul class="space-y-1 text-sm text-gray-300">
${stats.uniqueSizes.map((size: any) => `
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
`).join('')}
</ul>
</div>
</div>
</div>
`;
}
summaryContainer.innerHTML = summaryHTML;
if (stats.hasMixedSizes) {
createIcons({ icons });
}
}
function renderTable(unit: string) {
const tableBody = document.getElementById('dimensions-table-body');
if (!tableBody) return;
tableBody.textContent = '';
analyzedPagesData.forEach((pageData) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = document.createElement('tr');
const pageNumCell = document.createElement('td');
pageNumCell.className = 'px-4 py-3 text-white';
pageNumCell.textContent = pageData.pageNum;
const dimensionsCell = document.createElement('td');
dimensionsCell.className = 'px-4 py-3 text-gray-300';
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-3 text-gray-300';
sizeCell.textContent = pageData.standardSize;
const orientationCell = document.createElement('td');
orientationCell.className = 'px-4 py-3 text-gray-300';
orientationCell.textContent = pageData.orientation;
const aspectRatioCell = document.createElement('td');
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
aspectRatioCell.textContent = aspectRatio;
const areaCell = document.createElement('td');
areaCell.className = 'px-4 py-3 text-gray-300';
areaCell.textContent = area;
const rotationCell = document.createElement('td');
rotationCell.className = 'px-4 py-3 text-gray-300';
rotationCell.textContent = `${pageData.rotation}°`;
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell);
tableBody.appendChild(row);
});
}
function exportToCSV() {
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
const unit = unitsSelect?.value || 'pt';
const headers = ['Page #', `Width (${unit})`, `Height (${unit})`, 'Standard Size', 'Orientation', 'Aspect Ratio', `Area (${unit}²)`, 'Rotation'];
const csvRows = [headers.join(',')];
analyzedPagesData.forEach((pageData: any) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = [
pageData.pageNum,
width,
height,
pageData.standardSize,
pageData.orientation,
aspectRatio,
area,
`${pageData.rotation}°`
];
csvRows.push(row.join(','));
});
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'page-dimensions.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function analyzeAndDisplayDimensions() {
if (!pageState.pdfDoc) return;
analyzedPagesData = [];
const pages = pageState.pdfDoc.getPages();
pages.forEach((page: any, index: number) => {
const { width, height } = page.getSize();
const rotation = page.getRotation().angle || 0;
analyzedPagesData.push({
pageNum: index + 1,
width,
height,
orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height),
rotation: rotation
});
});
const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
renderSummary();
renderTable(unitsSelect.value);
if (resultsContainer) resultsContainer.classList.remove('hidden');
unitsSelect.addEventListener('change', (e) => {
renderTable((e.target as HTMLSelectElement).value);
});
const exportButton = document.getElementById('export-csv-btn');
if (exportButton) {
exportButton.addEventListener('click', exportToCSV);
}
createIcons({ icons });
}
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
analyzedPagesData = [];
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
const resultsContainer = document.getElementById('dimensions-results');
if (resultsContainer) resultsContainer.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function updateUI() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea) return;
fileDisplayArea.innerHTML = '';
if (pageState.file) {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = formatBytes(pageState.file.size);
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = function () {
resetState();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
}
async function handleFileSelect(files: FileList | null) {
if (files && files.length > 0) {
const file = files[0];
if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) {
pageState.file = file;
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFDocument.load(arrayBuffer);
updateUI();
analyzeAndDisplayDimensions();
} catch (e) {
console.error('Error loading PDF:', e);
showAlert('Error', 'Failed to load PDF file.');
}
}
}
}
document.addEventListener('DOMContentLoaded', function () {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', function () {
window.location.href = import.meta.env.BASE_URL;
});
}
if (fileInput && dropZone) {
fileInput.addEventListener('change', function (e) {
handleFileSelect((e.target as HTMLInputElement).files);
});
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('bg-gray-700');
});
dropZone.addEventListener('dragleave', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('bg-gray-700');
handleFileSelect(e.dataTransfer?.files);
});
fileInput.addEventListener('click', function () {
fileInput.value = '';
});
}
});

View File

@@ -1,258 +0,0 @@
import { state } from '../state.js';
import { getStandardPageName, convertPoints } from '../utils/helpers.js';
import { icons, createIcons } from 'lucide';
let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing
function calculateAspectRatio(width: number, height: number): string {
const ratio = width / height;
return ratio.toFixed(3);
}
function calculateArea(width: number, height: number, unit: string): string {
const areaInPoints = width * height;
let convertedArea = 0;
let unitSuffix = '';
switch (unit) {
case 'in':
convertedArea = areaInPoints / (72 * 72); // 72 points per inch
unitSuffix = 'in²';
break;
case 'mm':
convertedArea = areaInPoints / (72 * 72) * (25.4 * 25.4); // Convert to mm²
unitSuffix = 'mm²';
break;
case 'px':
const pxPerPoint = 96 / 72;
convertedArea = areaInPoints * (pxPerPoint * pxPerPoint);
unitSuffix = 'px²';
break;
default: // 'pt'
convertedArea = areaInPoints;
unitSuffix = 'pt²';
break;
}
return `${convertedArea.toFixed(2)} ${unitSuffix}`;
}
function getSummaryStats() {
const totalPages = analyzedPagesData.length;
// Count unique page sizes
const uniqueSizes = new Map();
analyzedPagesData.forEach((pageData: any) => {
const key = `${pageData.width.toFixed(2)}x${pageData.height.toFixed(2)}`;
const label = `${pageData.standardSize} (${pageData.orientation})`;
uniqueSizes.set(key, {
count: (uniqueSizes.get(key)?.count || 0) + 1,
label: label,
width: pageData.width,
height: pageData.height
});
});
const hasMixedSizes = uniqueSizes.size > 1;
return {
totalPages,
uniqueSizesCount: uniqueSizes.size,
uniqueSizes: Array.from(uniqueSizes.values()),
hasMixedSizes
};
}
function renderSummary() {
const summaryContainer = document.getElementById('dimensions-summary');
if (!summaryContainer) return;
const stats = getSummaryStats();
let summaryHTML = `
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Total Pages</p>
<p class="text-2xl font-bold text-white">${stats.totalPages}</p>
</div>
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Unique Page Sizes</p>
<p class="text-2xl font-bold text-white">${stats.uniqueSizesCount}</p>
</div>
<div class="bg-gray-900 border border-gray-700 rounded-lg p-4">
<p class="text-sm text-gray-400 mb-1">Document Type</p>
<p class="text-2xl font-bold ${stats.hasMixedSizes ? 'text-yellow-400' : 'text-green-400'}">
${stats.hasMixedSizes ? 'Mixed Sizes' : 'Uniform'}
</p>
</div>
</div>
`;
if (stats.hasMixedSizes) {
summaryHTML += `
<div class="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-4 mb-4">
<div class="flex items-start gap-3">
<i data-lucide="alert-triangle" class="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0"></i>
<div>
<h4 class="text-yellow-200 font-semibold mb-2">Mixed Page Sizes Detected</h4>
<p class="text-sm text-gray-300 mb-3">This document contains pages with different dimensions:</p>
<ul class="space-y-1 text-sm text-gray-300">
${stats.uniqueSizes.map((size: any) => `
<li>• ${size.label}: ${size.count} page${size.count > 1 ? 's' : ''}</li>
`).join('')}
</ul>
</div>
</div>
</div>
`;
}
summaryContainer.innerHTML = summaryHTML;
if (stats.hasMixedSizes) {
createIcons({ icons });
}
}
/**
* Renders the dimensions table based on the stored data and selected unit.
* @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px').
*/
function renderTable(unit: any) {
const tableBody = document.getElementById('dimensions-table-body');
if (!tableBody) return;
tableBody.textContent = ''; // Clear the table body safely
analyzedPagesData.forEach((pageData) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = document.createElement('tr');
// Page number
const pageNumCell = document.createElement('td');
pageNumCell.className = 'px-4 py-3 text-white';
pageNumCell.textContent = pageData.pageNum;
// Dimensions
const dimensionsCell = document.createElement('td');
dimensionsCell.className = 'px-4 py-3 text-gray-300';
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
// Standard size
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-3 text-gray-300';
sizeCell.textContent = pageData.standardSize;
// Orientation
const orientationCell = document.createElement('td');
orientationCell.className = 'px-4 py-3 text-gray-300';
orientationCell.textContent = pageData.orientation;
// Aspect Ratio
const aspectRatioCell = document.createElement('td');
aspectRatioCell.className = 'px-4 py-3 text-gray-300';
aspectRatioCell.textContent = aspectRatio;
// Area
const areaCell = document.createElement('td');
areaCell.className = 'px-4 py-3 text-gray-300';
areaCell.textContent = area;
// Rotation
const rotationCell = document.createElement('td');
rotationCell.className = 'px-4 py-3 text-gray-300';
rotationCell.textContent = `${pageData.rotation}°`;
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell, aspectRatioCell, areaCell, rotationCell);
tableBody.appendChild(row);
});
}
function exportToCSV() {
const unitsSelect = document.getElementById('units-select') as HTMLSelectElement;
const unit = unitsSelect?.value || 'pt';
const headers = ['Page #', `Width (${unit})`, `Height (${unit})`, 'Standard Size', 'Orientation', 'Aspect Ratio', `Area (${unit}²)`, 'Rotation'];
const csvRows = [headers.join(',')];
analyzedPagesData.forEach((pageData: any) => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const aspectRatio = calculateAspectRatio(pageData.width, pageData.height);
const area = calculateArea(pageData.width, pageData.height, unit);
const row = [
pageData.pageNum,
width,
height,
pageData.standardSize,
pageData.orientation,
aspectRatio,
area,
`${pageData.rotation}°`
];
csvRows.push(row.join(','));
});
const csvContent = csvRows.join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'page-dimensions.csv';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
/**
* Main function to analyze the PDF and display dimensions.
* This is called once after the file is loaded.
*/
export function analyzeAndDisplayDimensions() {
if (!state.pdfDoc) return;
analyzedPagesData = []; // Reset stored data
const pages = state.pdfDoc.getPages();
pages.forEach((page: any, index: any) => {
const { width, height } = page.getSize();
const rotation = page.getRotation().angle || 0;
analyzedPagesData.push({
pageNum: index + 1,
width, // Store raw width in points
height, // Store raw height in points
orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height),
rotation: rotation
});
});
const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById('units-select');
renderSummary();
// Initial render with default unit (points)
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
renderTable(unitsSelect.value);
resultsContainer.classList.remove('hidden');
unitsSelect.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
renderTable(e.target.value);
});
const exportButton = document.getElementById('export-csv-btn');
if (exportButton) {
exportButton.addEventListener('click', exportToCSV);
}
}

View File

@@ -0,0 +1,230 @@
import { createIcons, icons } from 'lucide';
import { showAlert, showLoader, hideLoader } from '../ui.js';
import { downloadFile, hexToRgb, formatBytes } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
interface PageState {
file: File | null;
pdfDoc: PDFLibDocument | null;
}
const pageState: PageState = {
file: null,
pdfDoc: null,
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializePage);
} else {
initializePage();
}
function initializePage() {
createIcons({ icons });
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const backBtn = document.getElementById('back-to-tools');
const processBtn = document.getElementById('process-btn');
if (fileInput) {
fileInput.addEventListener('change', handleFileUpload);
fileInput.addEventListener('click', () => { fileInput.value = ''; });
}
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-indigo-500');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-indigo-500');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-indigo-500');
if (e.dataTransfer?.files.length) {
handleFiles(e.dataTransfer.files);
}
});
}
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
if (processBtn) {
processBtn.addEventListener('click', addPageNumbers);
}
}
function handleFileUpload(e: Event) {
const input = e.target as HTMLInputElement;
if (input.files?.length) {
handleFiles(input.files);
}
}
async function handleFiles(files: FileList) {
const file = files[0];
if (!file || file.type !== 'application/pdf') {
showAlert('Invalid File', 'Please upload a valid PDF file.');
return;
}
showLoader('Loading PDF...');
try {
const arrayBuffer = await file.arrayBuffer();
pageState.pdfDoc = await PDFLibDocument.load(arrayBuffer);
pageState.file = file;
updateFileDisplay();
document.getElementById('options-panel')?.classList.remove('hidden');
} catch (error) {
console.error(error);
showAlert('Error', 'Failed to load PDF file.');
} finally {
hideLoader();
}
}
function updateFileDisplay() {
const fileDisplayArea = document.getElementById('file-display-area');
if (!fileDisplayArea || !pageState.file || !pageState.pdfDoc) return;
fileDisplayArea.innerHTML = '';
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col flex-1 min-w-0';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = pageState.file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(pageState.file.size)}${pageState.pdfDoc.getPageCount()} pages`;
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = resetState;
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
createIcons({ icons });
}
function resetState() {
pageState.file = null;
pageState.pdfDoc = null;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.innerHTML = '';
document.getElementById('options-panel')?.classList.add('hidden');
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
}
async function addPageNumbers() {
if (!pageState.pdfDoc) {
showAlert('Error', 'Please upload a PDF file first.');
return;
}
showLoader('Adding page numbers...');
try {
const position = (document.getElementById('position') as HTMLSelectElement).value;
const fontSize = parseInt((document.getElementById('font-size') as HTMLInputElement).value) || 12;
const format = (document.getElementById('number-format') as HTMLSelectElement).value;
const colorHex = (document.getElementById('text-color') as HTMLInputElement).value;
const textColor = hexToRgb(colorHex);
const pages = pageState.pdfDoc.getPages();
const totalPages = pages.length;
const helveticaFont = await pageState.pdfDoc.embedFont(StandardFonts.Helvetica);
for (let i = 0; i < totalPages; i++) {
const page = pages[i];
const mediaBox = page.getMediaBox();
const cropBox = page.getCropBox();
const bounds = cropBox || mediaBox;
const width = bounds.width;
const height = bounds.height;
const xOffset = bounds.x || 0;
const yOffset = bounds.y || 0;
let pageNumText = format === 'page_x_of_y' ? `${i + 1} / ${totalPages}` : `${i + 1}`;
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
const textHeight = fontSize;
const minMargin = 8;
const maxMargin = 40;
const marginPercentage = 0.04;
const horizontalMargin = Math.max(minMargin, Math.min(maxMargin, width * marginPercentage));
const verticalMargin = Math.max(minMargin, Math.min(maxMargin, height * marginPercentage));
const safeHorizontalMargin = Math.max(horizontalMargin, textWidth / 2 + 3);
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
let x = 0, y = 0;
switch (position) {
case 'bottom-center':
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-left':
x = safeHorizontalMargin + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-right':
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'top-center':
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-left':
x = safeHorizontalMargin + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-right':
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
}
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
page.drawText(pageNumText, {
x,
y,
font: helveticaFont,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b),
});
}
const newPdfBytes = await pageState.pdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'paginated.pdf');
showAlert('Success', 'Page numbers added successfully!', 'success', () => { resetState(); });
} catch (e) {
console.error(e);
showAlert('Error', 'Could not add page numbers.');
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,179 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let files: File[] = [];
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const dropZone = document.getElementById('drop-zone');
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
optionsPanel.classList.remove('hidden');
files.forEach((file) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
infoContainer.append(nameSpan, metaSpan);
// Add remove button
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = [];
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
// Fetch page count asynchronously
readFileAsArrayBuffer(file).then(buffer => {
return getPDFDocument(buffer).promise;
}).then(pdf => {
metaSpan.textContent = `${formatBytes(file.size)}${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
}).catch(e => {
console.warn('Error loading PDF page count:', e);
metaSpan.textContent = formatBytes(file.size);
});
});
// Initialize icons immediately after synchronous render
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
};
async function convert() {
if (files.length === 0) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Converting to BMP...');
try {
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(files[0])
).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context!, viewport: viewport, canvas }).promise;
const blob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, 'image/bmp')
);
if (blob) {
zip.file(`page_${i}.bmp`, blob);
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_images.zip');
showAlert('Success', 'PDF converted to BMPs successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to BMP. The file might be corrupted.'
);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'application/pdf'
);
if (validFiles.length === 0) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
files = [validFiles[0]];
updateUI();
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
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');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -1,98 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
/**
* Creates a BMP file buffer from raw pixel data (ImageData).
* This function is self-contained and has no external dependencies.
* @param {ImageData} imageData The pixel data from a canvas context.
* @returns {ArrayBuffer} The complete BMP file as an ArrayBuffer.
*/
function encodeBMP(imageData: any) {
const { width, height, data } = imageData;
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
const fileSize = stride * height + 54; // 54 byte header
const buffer = new ArrayBuffer(fileSize);
const view = new DataView(buffer);
// BMP File Header (14 bytes)
view.setUint16(0, 0x4d42, true); // 'BM'
view.setUint32(2, fileSize, true);
view.setUint32(10, 54, true); // Offset to pixel data
// DIB Header (BITMAPINFOHEADER) (40 bytes)
view.setUint32(14, 40, true); // DIB header size
view.setUint32(18, width, true);
view.setUint32(22, -height, true); // Negative height for top-down scanline order
view.setUint16(26, 1, true); // Color planes
view.setUint16(28, 24, true); // Bits per pixel
view.setUint32(30, 0, true); // No compression
view.setUint32(34, stride * height, true); // Image size
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
// Pixel Data
let offset = 54;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// BMP is BGR, not RGB
view.setUint8(offset++, data[i + 2]); // Blue
view.setUint8(offset++, data[i + 1]); // Green
view.setUint8(offset++, data[i]); // Red
}
// Add padding to make the row a multiple of 4 bytes
for (let p = 0; p < stride - width * 3; p++) {
view.setUint8(offset++, 0);
}
}
return buffer;
}
export async function pdfToBmp() {
showLoader('Converting PDF to BMP images...');
try {
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render the PDF page directly to the canvas
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
// Get the raw pixel data from this canvas
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// Use our new self-contained function to create the BMP file
const bmpBuffer = encodeBMP(imageData);
// Add the generated BMP file to the zip archive
zip.file(`page_${i}.bmp`, bmpBuffer);
}
showLoader('Compressing files into a ZIP...');
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_bmp_images.zip');
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to BMP. The file might be corrupted.'
);
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,206 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import { PDFDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let files: File[] = [];
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const dropZone = document.getElementById('drop-zone');
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
optionsPanel.classList.remove('hidden');
// Render files synchronously first
files.forEach((file) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = [];
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
// Fetch page count asynchronously
readFileAsArrayBuffer(file).then(buffer => {
return getPDFDocument(buffer).promise;
}).then(pdf => {
metaSpan.textContent = `${formatBytes(file.size)}${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
}).catch(e => {
console.warn('Error loading PDF page count:', e);
metaSpan.textContent = formatBytes(file.size);
});
});
// Initialize icons immediately after synchronous render
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
};
async function convert() {
if (files.length === 0) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Converting to greyscale...');
try {
const pdfBytes = await readFileAsArrayBuffer(files[0]) as ArrayBuffer;
const pdfDoc = await PDFDocument.load(pdfBytes);
const pages = pdfDoc.getPages();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
const newPdfDoc = await PDFDocument.create();
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context!, viewport: viewport, canvas }).promise;
const imageData = context!.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// Convert to greyscale
for (let j = 0; j < data.length; j += 4) {
const grey = Math.round(0.299 * data[j] + 0.587 * data[j + 1] + 0.114 * data[j + 2]);
data[j] = grey;
data[j + 1] = grey;
data[j + 2] = grey;
}
context!.putImageData(imageData, 0, 0);
const jpegBlob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', 0.9)
);
if (jpegBlob) {
const jpegBytes = await jpegBlob.arrayBuffer();
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(jpegImage, {
x: 0,
y: 0,
width: viewport.width,
height: viewport.height,
});
}
}
const resultBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(resultBytes)], { type: 'application/pdf' }),
'greyscale.pdf'
);
showAlert('Success', 'PDF converted to greyscale successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to greyscale. The file might be corrupted.'
);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'application/pdf'
);
if (validFiles.length === 0) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
files = [validFiles[0]];
updateUI();
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
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');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -1,69 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToGreyscale() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
showLoader('Converting to greyscale...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
const pdfjsDoc = await getPDFDocument({ data: pdfBytes }).promise;
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport, canvas }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
data[j] = avg; // red
data[j + 1] = avg; // green
data[j + 2] = avg; // blue
}
ctx.putImageData(imageData, 0, 0);
const imageBytes = await new Promise((resolve) =>
canvas.toBlob((blob) => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png')
);
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
const newPage = newPdfDoc.addPage([image.width, image.height]);
newPage.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height,
});
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
'greyscale.pdf'
);
} catch (e) {
console.error(e);
showAlert('Error', 'Could not convert to greyscale.');
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,193 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let files: File[] = [];
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const dropZone = document.getElementById('drop-zone');
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
optionsPanel.classList.remove('hidden');
files.forEach((file) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = [];
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
// Fetch page count asynchronously
readFileAsArrayBuffer(file).then(buffer => {
return getPDFDocument(buffer).promise;
}).then(pdf => {
metaSpan.textContent = `${formatBytes(file.size)}${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
}).catch(e => {
console.warn('Error loading PDF page count:', e);
metaSpan.textContent = formatBytes(file.size);
});
});
// Initialize icons immediately after synchronous render
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
const qualityValue = document.getElementById('jpg-quality-value');
if (qualitySlider) qualitySlider.value = '0.9';
if (qualityValue) qualityValue.textContent = '90%';
updateUI();
};
async function convert() {
if (files.length === 0) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Converting to JPG...');
try {
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(files[0])
).promise;
const zip = new JSZip();
const qualityInput = document.getElementById('jpg-quality') as HTMLInputElement;
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context!, viewport: viewport, canvas }).promise;
const blob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', quality)
);
if (blob) {
zip.file(`page_${i}.jpg`, blob);
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_images.zip');
showAlert('Success', 'PDF converted to JPGs successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to JPG. The file might be corrupted.'
);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const qualitySlider = document.getElementById('jpg-quality') as HTMLInputElement;
const qualityValue = document.getElementById('jpg-quality-value');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
if (qualitySlider && qualityValue) {
qualitySlider.addEventListener('input', () => {
qualityValue.textContent = `${Math.round(parseFloat(qualitySlider.value) * 100)}%`;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'application/pdf'
);
if (validFiles.length === 0) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
files = [validFiles[0]];
updateUI();
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
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');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -1,48 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToJpg() {
showLoader('Converting to JPG...');
try {
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
const qualityInput = document.getElementById('jpg-quality') as HTMLInputElement;
const quality = qualityInput ? parseFloat(qualityInput.value) : 0.9;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', quality)
);
zip.file(`page_${i}.jpg`, blob as Blob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_images.zip');
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to JPG. The file might be corrupted.'
);
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,193 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let files: File[] = [];
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const dropZone = document.getElementById('drop-zone');
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
optionsPanel.classList.remove('hidden');
files.forEach((file) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = [];
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
// Fetch page count asynchronously
readFileAsArrayBuffer(file).then(buffer => {
return getPDFDocument(buffer).promise;
}).then(pdf => {
metaSpan.textContent = `${formatBytes(file.size)}${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
}).catch(e => {
console.warn('Error loading PDF page count:', e);
metaSpan.textContent = formatBytes(file.size);
});
});
// Initialize icons immediately after synchronous render
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
const scaleSlider = document.getElementById('png-scale') as HTMLInputElement;
const scaleValue = document.getElementById('png-scale-value');
if (scaleSlider) scaleSlider.value = '2.0';
if (scaleValue) scaleValue.textContent = '2.0x';
updateUI();
};
async function convert() {
if (files.length === 0) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Converting to PNG...');
try {
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(files[0])
).promise;
const zip = new JSZip();
const scaleInput = document.getElementById('png-scale') as HTMLInputElement;
const scale = scaleInput ? parseFloat(scaleInput.value) : 2.0;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context!, viewport: viewport, canvas }).promise;
const blob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, 'image/png')
);
if (blob) {
zip.file(`page_${i}.png`, blob);
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_images.zip');
showAlert('Success', 'PDF converted to PNGs successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to PNG. The file might be corrupted.'
);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
const scaleSlider = document.getElementById('png-scale') as HTMLInputElement;
const scaleValue = document.getElementById('png-scale-value');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
if (scaleSlider && scaleValue) {
scaleSlider.addEventListener('input', () => {
scaleValue.textContent = `${parseFloat(scaleSlider.value).toFixed(1)}x`;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'application/pdf'
);
if (validFiles.length === 0) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
files = [validFiles[0]];
updateUI();
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
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');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

View File

@@ -1,42 +0,0 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
export async function pdfToPng() {
showLoader('Converting to PNG...');
try {
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(state.files[0])
).promise;
const zip = new JSZip();
const qualityInput = document.getElementById('png-quality') as HTMLInputElement;
const scale = qualityInput ? parseFloat(qualityInput.value) : 2.0;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport, canvas }).promise;
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/png')
);
zip.file(`page_${i}.png`, blob as Blob);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_pngs.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to PNG.');
} finally {
hideLoader();
}
}

View File

@@ -0,0 +1,190 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, formatBytes, readFileAsArrayBuffer, getPDFDocument } from '../utils/helpers.js';
import { createIcons, icons } from 'lucide';
import JSZip from 'jszip';
import * as pdfjsLib from 'pdfjs-dist';
import UTIF from 'utif';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).toString();
let files: File[] = [];
const updateUI = () => {
const fileDisplayArea = document.getElementById('file-display-area');
const optionsPanel = document.getElementById('options-panel');
const dropZone = document.getElementById('drop-zone');
if (!fileDisplayArea || !optionsPanel || !dropZone) return;
fileDisplayArea.innerHTML = '';
if (files.length > 0) {
optionsPanel.classList.remove('hidden');
files.forEach((file) => {
const fileDiv = document.createElement('div');
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
const infoContainer = document.createElement('div');
infoContainer.className = 'flex flex-col overflow-hidden';
const nameSpan = document.createElement('div');
nameSpan.className = 'truncate font-medium text-gray-200 text-sm mb-1';
nameSpan.textContent = file.name;
const metaSpan = document.createElement('div');
metaSpan.className = 'text-xs text-gray-400';
metaSpan.textContent = `${formatBytes(file.size)} • Loading pages...`; // Initial state
infoContainer.append(nameSpan, metaSpan);
const removeBtn = document.createElement('button');
removeBtn.className = 'ml-4 text-red-400 hover:text-red-300 flex-shrink-0';
removeBtn.innerHTML = '<i data-lucide="trash-2" class="w-4 h-4"></i>';
removeBtn.onclick = () => {
files = [];
updateUI();
};
fileDiv.append(infoContainer, removeBtn);
fileDisplayArea.appendChild(fileDiv);
// Fetch page count asynchronously
readFileAsArrayBuffer(file).then(buffer => {
return getPDFDocument(buffer).promise;
}).then(pdf => {
metaSpan.textContent = `${formatBytes(file.size)}${pdf.numPages} page${pdf.numPages !== 1 ? 's' : ''}`;
}).catch(e => {
console.warn('Error loading PDF page count:', e);
metaSpan.textContent = formatBytes(file.size);
});
});
// Initialize icons immediately after synchronous render
createIcons({ icons });
} else {
optionsPanel.classList.add('hidden');
}
};
const resetState = () => {
files = [];
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (fileInput) fileInput.value = '';
updateUI();
};
async function convert() {
if (files.length === 0) {
showAlert('No File', 'Please upload a PDF file first.');
return;
}
showLoader('Converting to TIFF...');
try {
const pdf = await getPDFDocument(
await readFileAsArrayBuffer(files[0])
).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context!, viewport: viewport, canvas }).promise;
const imageData = context!.getImageData(0, 0, canvas.width, canvas.height);
const rgba = imageData.data;
try {
const tiffData = UTIF.encodeImage(new Uint8Array(rgba), canvas.width, canvas.height);
const tiffBlob = new Blob([tiffData], { type: 'image/tiff' });
zip.file(`page_${i}.tiff`, tiffBlob);
} catch (encodeError: any) {
console.warn(`TIFF encoding failed for page ${i}, using PNG fallback:`, encodeError);
// Fallback to PNG if TIFF encoding fails (e.g., PackBits compression issues)
const pngBlob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, 'image/png')
);
if (pngBlob) {
zip.file(`page_${i}.png`, pngBlob);
}
}
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'converted_images.zip');
showAlert('Success', 'PDF converted to TIFFs successfully!', 'success', () => {
resetState();
});
} catch (e) {
console.error(e);
showAlert(
'Error',
'Failed to convert PDF to TIFF. The file might be corrupted.'
);
} finally {
hideLoader();
}
}
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('file-input') as HTMLInputElement;
const dropZone = document.getElementById('drop-zone');
const processBtn = document.getElementById('process-btn');
const backBtn = document.getElementById('back-to-tools');
if (backBtn) {
backBtn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
});
}
const handleFileSelect = (newFiles: FileList | null) => {
if (!newFiles || newFiles.length === 0) return;
const validFiles = Array.from(newFiles).filter(
(file) => file.type === 'application/pdf'
);
if (validFiles.length === 0) {
showAlert('Invalid File', 'Please upload a PDF file.');
return;
}
files = [validFiles[0]];
updateUI();
};
if (fileInput && dropZone) {
fileInput.addEventListener('change', (e) => {
handleFileSelect((e.target as HTMLInputElement).files);
});
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');
handleFileSelect(e.dataTransfer?.files ?? null);
});
fileInput.addEventListener('click', () => {
fileInput.value = '';
});
}
if (processBtn) {
processBtn.addEventListener('click', convert);
}
});

Some files were not shown because too many files have changed in this diff Show More