mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-10 09:14:34 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3e7ef25fa | ||
|
|
ba20536436 | ||
|
|
e3468e3aaa | ||
|
|
e0d3075609 | ||
|
|
466646d15b | ||
|
|
be6c15fef2 | ||
|
|
d5a15bce78 | ||
|
|
3cbbc42434 | ||
|
|
06c36a590b | ||
|
|
de3c2fecc2 | ||
|
|
b7de4bf1ed | ||
|
|
e0e5d50240 | ||
|
|
11ba3fe81b | ||
|
|
05de28d791 | ||
|
|
0403a9665f | ||
|
|
0ddf54cba3 | ||
|
|
25c221f8b9 | ||
|
|
d4f1aa30f5 | ||
|
|
c9faee4fd9 | ||
|
|
3fc873d343 | ||
|
|
739dac55ee | ||
|
|
dbc1b8eb7f | ||
|
|
11c24c1ff1 | ||
|
|
929aa7e132 | ||
|
|
2b85a759af | ||
|
|
9af9c33f74 | ||
|
|
835c3c9423 | ||
|
|
a8df98f20d | ||
|
|
11c9e14803 | ||
|
|
4e2d04fb9c | ||
|
|
edc0f27e0f | ||
|
|
bf57f36cb5 | ||
|
|
6ad6a7d730 | ||
|
|
0e7c53560a |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
github: alam00000
|
||||||
|
ko_fi: alio01
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: 'npm'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
|
groups:
|
||||||
|
npm-dependencies:
|
||||||
|
patterns:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
- package-ecosystem: 'github-actions'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: build docker image and push to dockerhub
|
name: Build and Push Docker Images (Default + Simple Mode)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -6,6 +6,7 @@ on:
|
|||||||
- 'main'
|
- 'main'
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build-and-push:
|
docker-build-and-push:
|
||||||
@@ -13,6 +14,15 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
mode:
|
||||||
|
- name: default
|
||||||
|
simple_mode: false
|
||||||
|
suffix: ''
|
||||||
|
- name: simple
|
||||||
|
simple_mode: true
|
||||||
|
suffix: '-simple'
|
||||||
steps:
|
steps:
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -34,13 +44,17 @@ jobs:
|
|||||||
echo "version=latest" >> $GITHUB_OUTPUT
|
echo "version=latest" >> $GITHUB_OUTPUT
|
||||||
echo "version_without_v=latest" >> $GITHUB_OUTPUT
|
echo "version_without_v=latest" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push ${{ matrix.mode.name }} image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
|
build-args: |
|
||||||
|
SIMPLE_MODE=${{ matrix.mode.simple_mode }}
|
||||||
tags: |
|
tags: |
|
||||||
bentopdf/bentopdf:latest
|
bentopdf/bentopdf${{ matrix.mode.suffix }}:latest
|
||||||
bentopdf/bentopdf:${{ steps.version.outputs.version_without_v }}
|
bentopdf/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version_without_v }}
|
||||||
bentopdf/bentopdf:${{ steps.version.outputs.version }}
|
bentopdf/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
**BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that allows you to manipulate, edit, merge, and process PDF files directly in your browser. No server-side processing is required, ensuring your files remain secure and private.
|
**BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that allows you to manipulate, edit, merge, and process PDF files directly in your browser. No server-side processing is required, ensuring your files remain secure and private.
|
||||||
|
|
||||||
 [](https://ko-fi.com/alio0) 
|
 [](https://ko-fi.com/alio0) 
|
||||||
|
[](https://github.com/sponsors/alam00000)
|
||||||
|
|
||||||
## ⭐ Stargazers over time
|
## ⭐ Stargazers over time
|
||||||
|
|
||||||
@@ -25,16 +26,18 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
|||||||
|
|
||||||
### Organize & Manage PDFs
|
### Organize & Manage PDFs
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :----------------- | :------------------------------------------------------------------------- |
|
| :------------------------ | :------------------------------------------------------------------------- |
|
||||||
| **Merge PDFs** | Combine multiple PDF files into one. |
|
| **Merge PDFs** | Combine multiple PDF files into one. |
|
||||||
| **Split PDFs** | Extract specific pages or divide a document into smaller files. |
|
| **Split PDFs** | Extract specific pages or divide a document into smaller files. |
|
||||||
| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. |
|
| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. |
|
||||||
| **Extract Pages** | Save a specific range of pages as a new PDF. |
|
| **Extract Pages** | Save a specific range of pages as a new PDF. |
|
||||||
| **Delete Pages** | Remove unwanted pages from your document. |
|
| **Delete Pages** | Remove unwanted pages from your document. |
|
||||||
| **Rotate PDF** | Rotate individual or all pages in a document. |
|
| **Rotate PDF** | Rotate individual or all pages in a document. |
|
||||||
| **N-Up PDF** | Combine multiple pages onto a single page. |
|
| **N-Up PDF** | Combine multiple pages onto a single page. |
|
||||||
| **View PDF** | A powerful, integrated PDF viewer. |
|
| **View PDF** | A powerful, integrated PDF viewer. |
|
||||||
|
| **Alternate & Mix pages** | Merge pages by alternating pages from each PDF. |
|
||||||
|
| **Posterize PDF** | Split a PDF into multiple smaller pages for print. |
|
||||||
|
|
||||||
### Edit & Modify PDFs
|
### Edit & Modify PDFs
|
||||||
|
|
||||||
@@ -51,6 +54,7 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
|||||||
| **Fill Forms** | Fill out PDF forms directly in your browser. |
|
| **Fill Forms** | Fill out PDF forms directly in your browser. |
|
||||||
| **Flatten PDF** | Flatten form fields and annotations into static content. |
|
| **Flatten PDF** | Flatten form fields and annotations into static content. |
|
||||||
| **Remove Annotations** | Remove comments, highlights, and other annotations. |
|
| **Remove Annotations** | Remove comments, highlights, and other annotations. |
|
||||||
|
| **Remove Blank Pages** | Auto detect and remove blank pages in a PDF. |
|
||||||
|
|
||||||
### Convert to PDF
|
### Convert to PDF
|
||||||
|
|
||||||
@@ -81,6 +85,8 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
|||||||
| **Redact Content** | Permanently remove sensitive content from your PDFs. |
|
| **Redact Content** | Permanently remove sensitive content from your PDFs. |
|
||||||
| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). |
|
| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). |
|
||||||
| **Remove Metadata** | Strip all metadata from your PDF for privacy. |
|
| **Remove Metadata** | Strip all metadata from your PDF for privacy. |
|
||||||
|
| **Linearize PDF** | Optimize PDF for fast web view. |
|
||||||
|
| **Sanitize PDF** | Remove potentially unwanted or malicous files from PDF. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -135,6 +141,7 @@ The application will be available at `http://localhost:3000`.
|
|||||||
For organizations that want a clean, distraction-free interface focused solely on PDF tools, BentoPDF supports a **Simple Mode** that hides all branding and marketing content.
|
For organizations that want a clean, distraction-free interface focused solely on PDF tools, BentoPDF supports a **Simple Mode** that hides all branding and marketing content.
|
||||||
|
|
||||||
**What Simple Mode does:**
|
**What Simple Mode does:**
|
||||||
|
|
||||||
- Hides navigation, hero section, features, FAQ, testimonials, and footer
|
- Hides navigation, hero section, features, FAQ, testimonials, and footer
|
||||||
- Shows only the essential PDF tools
|
- Shows only the essential PDF tools
|
||||||
- Updates page title to "PDF Tools"
|
- Updates page title to "PDF Tools"
|
||||||
@@ -162,8 +169,9 @@ npm run release:minor
|
|||||||
# Release a major version (0.0.1 → 1.0.0)
|
# Release a major version (0.0.1 → 1.0.0)
|
||||||
npm run release:major
|
npm run release:major
|
||||||
```
|
```
|
||||||
```
|
|
||||||
For detailed release instructions, see [RELEASE.md](RELEASE.md).
|
For detailed release instructions, see [RELEASE.md](RELEASE.md).
|
||||||
|
|
||||||
### 🚀 Development Setup
|
### 🚀 Development Setup
|
||||||
|
|
||||||
#### Option 1: Run with npm
|
#### Option 1: Run with npm
|
||||||
@@ -232,14 +240,12 @@ BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript*
|
|||||||
|
|
||||||
- **HTML to PDF**: Convert HTML files or web pages into PDF documents.
|
- **HTML to PDF**: Convert HTML files or web pages into PDF documents.
|
||||||
- **Markdown to PDF**: Enhanced support for converting `.md` files to PDF.
|
- **Markdown to PDF**: Enhanced support for converting `.md` files to PDF.
|
||||||
- **Sanitize PDF**: Remove potentially malicious content like scripts from PDFs.
|
|
||||||
- **Convert to PDF/A**: Convert PDFs to the PDF/A archival format.
|
- **Convert to PDF/A**: Convert PDFs to the PDF/A archival format.
|
||||||
- **Edit PDF Content**: Directly edit text and other content within your PDF.
|
- **Edit PDF Content**: Directly edit text and other content within your PDF.
|
||||||
- **Linearize PDF**: Optimize PDFs for fast web viewing.
|
|
||||||
- **PDF to Office**: Converts PDF files into editable Word, Excel, and PowerPoint formats.
|
- **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.
|
- **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/q42xWQmJ).
|
Contributions and discussions on the roadmap are welcome! Join the conversation via [Discord](https://discord.gg/AP2Y97juZT).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -263,10 +269,11 @@ BentoPDF wouldn't be possible without the amazing open-source tools and librarie
|
|||||||
- **[PDFLib.js](https://pdf-lib.js.org/)** – For enabling powerful client-side PDF manipulation.
|
- **[PDFLib.js](https://pdf-lib.js.org/)** – For enabling powerful client-side PDF manipulation.
|
||||||
- **[PDF.js](https://mozilla.github.io/pdf.js/)** – For the robust PDF rendering engine in the browser.
|
- **[PDF.js](https://mozilla.github.io/pdf.js/)** – For the robust PDF rendering engine in the browser.
|
||||||
- **[PDFKit](https://pdfkit.org/)** – For creating and editing PDFs with ease.
|
- **[PDFKit](https://pdfkit.org/)** – For creating and editing PDFs with ease.
|
||||||
- **[EmbedPDF](https://github.com/embedpdf/embed-pdf-viewer)** – For seamless PDF embedding in web pages.
|
- **[EmbedPDF](https://github.com/embedpdf/embed-pdf-viewer)** – For seamless PDF editing in pure JS.
|
||||||
- **[Cropper.js](https://fengyuanchen.github.io/cropperjs/)** – For intuitive image cropping functionality.
|
- **[Cropper.js](https://fengyuanchen.github.io/cropperjs/)** – For intuitive image cropping functionality.
|
||||||
- **[Vite](https://vitejs.dev/)** – For lightning-fast development and build tooling.
|
- **[Vite](https://vitejs.dev/)** – For lightning-fast development and build tooling.
|
||||||
- **[Tailwind CSS](https://tailwindcss.com/)** – For rapid, flexible, and beautiful UI styling.
|
- **[Tailwind CSS](https://tailwindcss.com/)** – For rapid, flexible, and beautiful UI styling.
|
||||||
|
- **[qpdf](https://github.com/qpdf/qpdf)** and **[qpdf-wasm](https://github.com/neslinesli93/qpdf-wasm)**– A powerful command-line tool and library for inspecting, repairing, and transforming PDF file ported to wasm
|
||||||
|
|
||||||
Your work inspires and empowers developers everywhere. Thank you for making open-source amazing!
|
Your work inspires and empowers developers everywhere. Thank you for making open-source amazing!
|
||||||
|
|
||||||
|
|||||||
+36
-9
@@ -7,6 +7,7 @@
|
|||||||
**Situation:** You've completed a new feature, tested it locally, and want to release it.
|
**Situation:** You've completed a new feature, tested it locally, and want to release it.
|
||||||
|
|
||||||
**Current State:**
|
**Current State:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ git status
|
$ git status
|
||||||
On branch main
|
On branch main
|
||||||
@@ -22,6 +23,7 @@ Untracked files:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Commit your feature changes
|
# 1. Commit your feature changes
|
||||||
git add .
|
git add .
|
||||||
@@ -34,6 +36,7 @@ npm run release:major # Major: 1.0.0 → 2.0.0 (breaking changes)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**What Happens:**
|
**What Happens:**
|
||||||
|
|
||||||
- ✅ Your feature commit stays as-is
|
- ✅ Your feature commit stays as-is
|
||||||
- ✅ Version gets bumped in `package.json`
|
- ✅ Version gets bumped in `package.json`
|
||||||
- ✅ New release commit is created
|
- ✅ New release commit is created
|
||||||
@@ -48,6 +51,7 @@ npm run release:major # Major: 1.0.0 → 2.0.0 (breaking changes)
|
|||||||
**Situation:** You have local changes but haven't committed them yet.
|
**Situation:** You have local changes but haven't committed them yet.
|
||||||
|
|
||||||
**Current State:**
|
**Current State:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ git status
|
$ git status
|
||||||
Changes not staged for commit:
|
Changes not staged for commit:
|
||||||
@@ -57,6 +61,7 @@ Changes not staged for commit:
|
|||||||
```
|
```
|
||||||
|
|
||||||
**❌ This Will Fail:**
|
**❌ This Will Fail:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run release
|
npm run release
|
||||||
# Error: Your local changes would be overwritten by merge
|
# Error: Your local changes would be overwritten by merge
|
||||||
@@ -65,6 +70,7 @@ npm run release
|
|||||||
**✅ Solution Options:**
|
**✅ Solution Options:**
|
||||||
|
|
||||||
**Option A: Commit Everything First (Recommended)**
|
**Option A: Commit Everything First (Recommended)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Add new features and improvements"
|
git commit -m "Add new features and improvements"
|
||||||
@@ -72,6 +78,7 @@ npm run release
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Option B: Stash Changes Temporarily**
|
**Option B: Stash Changes Temporarily**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git stash
|
git stash
|
||||||
npm run release
|
npm run release
|
||||||
@@ -79,6 +86,7 @@ git stash pop # Restore your changes after release
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Option C: Commit Only What's Needed**
|
**Option C: Commit Only What's Needed**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add package.json src/js/main.js
|
git add package.json src/js/main.js
|
||||||
git commit -m "Add core improvements"
|
git commit -m "Add core improvements"
|
||||||
@@ -94,6 +102,7 @@ git commit -m "Update documentation"
|
|||||||
**Situation:** There's a critical bug in production that needs immediate fixing.
|
**Situation:** There's a critical bug in production that needs immediate fixing.
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Fix the bug
|
# 1. Fix the bug
|
||||||
git add src/js/bug-fix.js
|
git add src/js/bug-fix.js
|
||||||
@@ -105,6 +114,7 @@ npm run release
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Result:**
|
**Result:**
|
||||||
|
|
||||||
- ✅ Bug fix gets released immediately
|
- ✅ Bug fix gets released immediately
|
||||||
- ✅ Docker image with fix is available
|
- ✅ Docker image with fix is available
|
||||||
- ✅ Users can pull the fixed version
|
- ✅ Users can pull the fixed version
|
||||||
@@ -116,6 +126,7 @@ npm run release
|
|||||||
**Situation:** You've added significant new features that might break existing functionality.
|
**Situation:** You've added significant new features that might break existing functionality.
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Commit all your changes
|
# 1. Commit all your changes
|
||||||
git add .
|
git add .
|
||||||
@@ -127,6 +138,7 @@ npm run release:major
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Result:**
|
**Result:**
|
||||||
|
|
||||||
- ✅ Major version bump indicates breaking changes
|
- ✅ Major version bump indicates breaking changes
|
||||||
- ✅ Users know to check compatibility
|
- ✅ Users know to check compatibility
|
||||||
- ✅ Both old and new versions available
|
- ✅ Both old and new versions available
|
||||||
@@ -138,6 +150,7 @@ npm run release:major
|
|||||||
**Situation:** You've been working on multiple features and want to release them together.
|
**Situation:** You've been working on multiple features and want to release them together.
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Commit all features
|
# 1. Commit all features
|
||||||
git add .
|
git add .
|
||||||
@@ -156,6 +169,7 @@ npm run release:major # For breaking changes (1.0.0 → 2.0.0)
|
|||||||
**Situation:** You want to test the release system without affecting production.
|
**Situation:** You want to test the release system without affecting production.
|
||||||
|
|
||||||
**Steps:**
|
**Steps:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Make a small test change
|
# 1. Make a small test change
|
||||||
echo "// Test comment" >> src/js/main.js
|
echo "// Test comment" >> src/js/main.js
|
||||||
@@ -179,23 +193,25 @@ git reset --hard HEAD~1
|
|||||||
|
|
||||||
## 🎯 **Release Type Guidelines**
|
## 🎯 **Release Type Guidelines**
|
||||||
|
|
||||||
| Scenario | Command | Version Change | When to Use |
|
| Scenario | Command | Version Change | When to Use |
|
||||||
|----------|---------|----------------|-------------|
|
| ------------------- | ----------------------- | --------------- | ------------------------------------ |
|
||||||
| **Bug Fix** | `npm run release` | `1.0.0 → 1.0.1` | Fixing bugs, small improvements |
|
| **Bug Fix** | `npm run release` | `1.0.0 → 1.0.1` | Fixing bugs, small improvements |
|
||||||
| **New Feature** | `npm run release:minor` | `1.0.0 → 1.1.0` | Adding features, backward compatible |
|
| **New Feature** | `npm run release:minor` | `1.0.0 → 1.1.0` | Adding features, backward compatible |
|
||||||
| **Breaking Change** | `npm run release:major` | `1.0.0 → 2.0.0` | API changes, major rewrites |
|
| **Breaking Change** | `npm run release:major` | `1.0.0 → 2.0.0` | API changes, major rewrites |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔄 **What Happens After You Run a Release Command**
|
## 🔄 **What Happens After You Run a Release Command**
|
||||||
|
|
||||||
### **Immediate Actions (Local):**
|
### **Immediate Actions (Local):**
|
||||||
|
|
||||||
1. **Version Update**: `package.json` version gets bumped
|
1. **Version Update**: `package.json` version gets bumped
|
||||||
2. **Git Commit**: New commit created with "Release vX.X.X"
|
2. **Git Commit**: New commit created with "Release vX.X.X"
|
||||||
3. **Git Tag**: Tag created (e.g., `v1.0.1`)
|
3. **Git Tag**: Tag created (e.g., `v1.0.1`)
|
||||||
4. **Git Push**: Everything pushed to GitHub
|
4. **Git Push**: Everything pushed to GitHub
|
||||||
|
|
||||||
### **Automatic Actions (GitHub):**
|
### **Automatic Actions (GitHub):**
|
||||||
|
|
||||||
1. **GitHub Actions Triggered**: Workflow starts building Docker image
|
1. **GitHub Actions Triggered**: Workflow starts building Docker image
|
||||||
2. **Docker Build**: Multi-architecture image created
|
2. **Docker Build**: Multi-architecture image created
|
||||||
3. **Docker Push**: Images pushed to Docker Hub with tags:
|
3. **Docker Push**: Images pushed to Docker Hub with tags:
|
||||||
@@ -204,7 +220,9 @@ git reset --hard HEAD~1
|
|||||||
- `bentopdf/bentopdf:v1.0.1`
|
- `bentopdf/bentopdf:v1.0.1`
|
||||||
|
|
||||||
### **End Result:**
|
### **End Result:**
|
||||||
|
|
||||||
Users can immediately pull your new version:
|
Users can immediately pull your new version:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull bentopdf/bentopdf:1.0.1
|
docker pull bentopdf/bentopdf:1.0.1
|
||||||
```
|
```
|
||||||
@@ -214,6 +232,7 @@ docker pull bentopdf/bentopdf:1.0.1
|
|||||||
## 🚨 **Before You Release - Prerequisites**
|
## 🚨 **Before You Release - Prerequisites**
|
||||||
|
|
||||||
### **1. Docker Hub Credentials Setup**
|
### **1. Docker Hub Credentials Setup**
|
||||||
|
|
||||||
You need to add these secrets to your GitHub repository:
|
You need to add these secrets to your GitHub repository:
|
||||||
|
|
||||||
1. Go to **Settings** → **Secrets and variables** → **Actions**
|
1. Go to **Settings** → **Secrets and variables** → **Actions**
|
||||||
@@ -222,6 +241,7 @@ You need to add these secrets to your GitHub repository:
|
|||||||
- `DOCKER_TOKEN`: Your Docker Hub access token
|
- `DOCKER_TOKEN`: Your Docker Hub access token
|
||||||
|
|
||||||
### **2. Get Docker Hub Token**
|
### **2. Get Docker Hub Token**
|
||||||
|
|
||||||
1. Go to [Docker Hub](https://hub.docker.com)
|
1. Go to [Docker Hub](https://hub.docker.com)
|
||||||
2. Account Settings → Security → New Access Token
|
2. Account Settings → Security → New Access Token
|
||||||
3. Set permissions to "Read, Write, Delete"
|
3. Set permissions to "Read, Write, Delete"
|
||||||
@@ -232,8 +252,10 @@ You need to add these secrets to your GitHub repository:
|
|||||||
## 🔧 **Troubleshooting Common Issues**
|
## 🔧 **Troubleshooting Common Issues**
|
||||||
|
|
||||||
### **❌ "Your local changes would be overwritten by merge"**
|
### **❌ "Your local changes would be overwritten by merge"**
|
||||||
|
|
||||||
**Problem:** You have uncommitted changes
|
**Problem:** You have uncommitted changes
|
||||||
**Solution:**
|
**Solution:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Your commit message"
|
git commit -m "Your commit message"
|
||||||
@@ -241,16 +263,20 @@ npm run release
|
|||||||
```
|
```
|
||||||
|
|
||||||
### **❌ "Permission denied" in GitHub Actions**
|
### **❌ "Permission denied" in GitHub Actions**
|
||||||
|
|
||||||
**Problem:** Missing Docker Hub credentials
|
**Problem:** Missing Docker Hub credentials
|
||||||
**Solution:** Add `DOCKER_USERNAME` and `DOCKER_TOKEN` to GitHub Secrets
|
**Solution:** Add `DOCKER_USERNAME` and `DOCKER_TOKEN` to GitHub Secrets
|
||||||
|
|
||||||
### **❌ "Tag already exists"**
|
### **❌ "Tag already exists"**
|
||||||
|
|
||||||
**Problem:** You've run the same release before
|
**Problem:** You've run the same release before
|
||||||
**Solution:** This is normal! The script will skip creating duplicate tags
|
**Solution:** This is normal! The script will skip creating duplicate tags
|
||||||
|
|
||||||
### **❌ GitHub Actions fails**
|
### **❌ GitHub Actions fails**
|
||||||
|
|
||||||
**Problem:** Various build issues
|
**Problem:** Various build issues
|
||||||
**Solution:**
|
**Solution:**
|
||||||
|
|
||||||
1. Check Actions tab for detailed logs
|
1. Check Actions tab for detailed logs
|
||||||
2. Verify Docker Hub credentials
|
2. Verify Docker Hub credentials
|
||||||
3. Check Dockerfile for syntax errors
|
3. Check Dockerfile for syntax errors
|
||||||
@@ -260,6 +286,7 @@ npm run release
|
|||||||
## 🧪 **Testing Your Release System**
|
## 🧪 **Testing Your Release System**
|
||||||
|
|
||||||
### **Quick Test:**
|
### **Quick Test:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Make a small change
|
# Make a small change
|
||||||
echo "// Test" >> src/js/main.js
|
echo "// Test" >> src/js/main.js
|
||||||
@@ -269,12 +296,14 @@ npm run release
|
|||||||
```
|
```
|
||||||
|
|
||||||
### **Verify Results:**
|
### **Verify Results:**
|
||||||
|
|
||||||
1. **GitHub Actions**: Check Actions tab for successful build
|
1. **GitHub Actions**: Check Actions tab for successful build
|
||||||
2. **Docker Hub**: Verify images are published
|
2. **Docker Hub**: Verify images are published
|
||||||
3. **Git Tags**: `git tag --list` should show new tag
|
3. **Git Tags**: `git tag --list` should show new tag
|
||||||
4. **Version**: `cat package.json | grep version` should show updated version
|
4. **Version**: `cat package.json | grep version` should show updated version
|
||||||
|
|
||||||
### **Undo Test Release:**
|
### **Undo Test Release:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git tag -d v1.0.1
|
git tag -d v1.0.1
|
||||||
git push origin :refs/tags/v1.0.1
|
git push origin :refs/tags/v1.0.1
|
||||||
@@ -286,5 +315,3 @@ git reset --hard HEAD~1
|
|||||||
## 🎉 **That's It!**
|
## 🎉 **That's It!**
|
||||||
|
|
||||||
Your release system is now ready! Just follow the scenarios above based on your situation and run the appropriate `npm run release` command.
|
Your release system is now ready! Just follow the scenarios above based on your situation and run the appropriate `npm run release` command.
|
||||||
|
|
||||||
|
|
||||||
+105
-15
@@ -5,6 +5,7 @@ Simple Mode is designed for internal organizational use where you want to hide a
|
|||||||
## What Simple Mode Does
|
## What Simple Mode Does
|
||||||
|
|
||||||
When enabled, Simple Mode will:
|
When enabled, Simple Mode will:
|
||||||
|
|
||||||
- Hide the navigation bar
|
- Hide the navigation bar
|
||||||
- Hide the hero section with marketing content
|
- Hide the hero section with marketing content
|
||||||
- Hide the features section
|
- Hide the features section
|
||||||
@@ -18,36 +19,89 @@ When enabled, Simple Mode will:
|
|||||||
|
|
||||||
## How to Enable Simple Mode
|
## How to Enable Simple Mode
|
||||||
|
|
||||||
### Using Docker Compose
|
### Method 1: Using Pre-built Simple Mode Image (Recommended)
|
||||||
|
|
||||||
|
Use the pre-built Simple Mode image directly:
|
||||||
|
|
||||||
1. Create a `.env` file in your project root:
|
|
||||||
```bash
|
```bash
|
||||||
SIMPLE_MODE=true
|
docker run -p 3000:80 bentopdf/bentopdf-simple:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run with docker-compose:
|
Or with Docker Compose:
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
```yaml
|
||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
image: bentopdf/bentopdf-simple:latest
|
||||||
|
container_name: bentopdf
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '3000:80'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Docker Build
|
### Method 2: Using Docker Compose with Build
|
||||||
|
|
||||||
|
Build the image locally with Simple Mode enabled:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml build --build-arg SIMPLE_MODE=true
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 3: Using Docker Build
|
||||||
|
|
||||||
|
Build the image with the SIMPLE_MODE build argument:
|
||||||
|
|
||||||
Build the image with the SIMPLE_MODE environment variable:
|
|
||||||
```bash
|
```bash
|
||||||
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
|
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
|
||||||
|
docker run -p 3000:80 bentopdf-simple
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using Environment Variables
|
### Method 4: Using npm Script (Easiest for Local Development)
|
||||||
|
|
||||||
|
Use the built-in npm script that handles everything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run serve:simple
|
||||||
|
```
|
||||||
|
|
||||||
|
This command automatically:
|
||||||
|
|
||||||
|
- Sets `SIMPLE_MODE=true`
|
||||||
|
- Builds the project with Simple Mode enabled
|
||||||
|
- Serves the built files on `http://localhost:3000`
|
||||||
|
|
||||||
|
### Method 5: Using Environment Variables
|
||||||
|
|
||||||
Set the environment variable before building:
|
Set the environment variable before building:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SIMPLE_MODE=true
|
export SIMPLE_MODE=true
|
||||||
npm run build
|
npm run build
|
||||||
|
npx serve dist -p 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🧪 Testing Simple Mode Locally
|
## 🧪 Testing Simple Mode Locally
|
||||||
|
|
||||||
After building with Simple Mode enabled, you need to serve the built files locally.
|
### Method 1: Using npm Script (Easiest for Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run serve:simple
|
||||||
|
```
|
||||||
|
|
||||||
|
This automatically builds and serves Simple Mode on `http://localhost:3000`.
|
||||||
|
|
||||||
|
### Method 2: Using Pre-built Image (Easiest for Production)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull and run the Simple Mode image
|
||||||
|
docker pull bentopdf/bentopdf-simple:latest
|
||||||
|
docker run -p 3000:80 bentopdf/bentopdf-simple:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
|
### Method 3: Build and Test Locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build with simple mode
|
# Build with simple mode
|
||||||
@@ -56,11 +110,26 @@ SIMPLE_MODE=true npm run build
|
|||||||
# Serve the built files
|
# Serve the built files
|
||||||
npx serve dist -p 3000
|
npx serve dist -p 3000
|
||||||
```
|
```
|
||||||
Then open `http://localhost:3000` in your browser.
|
|
||||||
|
Open `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
|
### Method 4: Compare Both Modes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test Normal Mode
|
||||||
|
docker run -p 3000:80 bentopdf/bentopdf:latest
|
||||||
|
|
||||||
|
# Test Simple Mode
|
||||||
|
docker run -p 3001:80 bentopdf/bentopdf-simple:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
- Normal Mode: `http://localhost:3000`
|
||||||
|
- Simple Mode: `http://localhost:3001`
|
||||||
|
|
||||||
## 🔍 What to Look For
|
## 🔍 What to Look For
|
||||||
|
|
||||||
When Simple Mode is working correctly, you should see:
|
When Simple Mode is working correctly, you should see:
|
||||||
|
|
||||||
- ✅ Clean "PDF Tools" header (no marketing hero section)
|
- ✅ Clean "PDF Tools" header (no marketing hero section)
|
||||||
- ✅ "Select a tool to get started" subtitle
|
- ✅ "Select a tool to get started" subtitle
|
||||||
- ✅ Search bar for tools
|
- ✅ Search bar for tools
|
||||||
@@ -69,17 +138,38 @@ When Simple Mode is working correctly, you should see:
|
|||||||
- ❌ No hero section with "The PDF Toolkit built for privacy"
|
- ❌ No hero section with "The PDF Toolkit built for privacy"
|
||||||
- ❌ No features, FAQ, testimonials, or footer sections
|
- ❌ No features, FAQ, testimonials, or footer sections
|
||||||
|
|
||||||
## Example Docker Compose Configuration
|
## 📦 Available Docker Images
|
||||||
|
|
||||||
|
### Normal Mode (Full Branding)
|
||||||
|
|
||||||
|
- `bentopdf/bentopdf:latest`
|
||||||
|
- `bentopdf/bentopdf:v1.0.0` (versioned)
|
||||||
|
|
||||||
|
### Simple Mode (Clean Interface)
|
||||||
|
|
||||||
|
- `bentopdf/bentopdf-simple:latest`
|
||||||
|
- `bentopdf/bentopdf-simple:v1.0.0` (versioned)
|
||||||
|
|
||||||
|
## 🚀 Production Deployment Examples
|
||||||
|
|
||||||
|
### Internal Company Tool
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
bentopdf:
|
bentopdf:
|
||||||
image: bentopdf/bentopdf:latest
|
image: bentopdf/bentopdf-simple:latest
|
||||||
container_name: bentopdf
|
container_name: bentopdf
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- '3000:80'
|
- '80:80'
|
||||||
environment:
|
environment:
|
||||||
- SIMPLE_MODE=true
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## ⚠️ Important Notes
|
||||||
|
|
||||||
|
- **Pre-built images**: Use `bentopdf/bentopdf-simple:latest` for Simple Mode
|
||||||
|
- **Environment variables**: `SIMPLE_MODE=true` only works during build, not runtime
|
||||||
|
- **Build-time optimization**: Simple Mode uses dead code elimination for smaller bundles
|
||||||
|
- **Same functionality**: All PDF tools work identically in both modes
|
||||||
+83
-43
@@ -12,54 +12,94 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl ml-2"> <a href="index.html">BentoPDF</a> </span>
|
id="home-logo"
|
||||||
</div>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Mobile Hamburger Button -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="md:hidden flex items-center">
|
||||||
<button id="mobile-menu-button" type="button"
|
<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"
|
id="mobile-menu-button"
|
||||||
aria-controls="mobile-menu" aria-expanded="false">
|
type="button"
|
||||||
<span class="sr-only">Open main menu</span>
|
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"
|
||||||
<!-- Hamburger Icon -->
|
aria-controls="mobile-menu"
|
||||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
aria-expanded="false"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<span class="sr-only">Open main menu</span>
|
||||||
d="M4 6h16M4 12h16M4 18h16" />
|
<!-- Hamburger Icon -->
|
||||||
</svg>
|
<svg
|
||||||
<!-- Close Icon -->
|
id="menu-icon"
|
||||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
class="block h-6 w-6"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
fill="none"
|
||||||
d="M6 18L18 6M6 6l12 12" />
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
aria-hidden="true"
|
||||||
</div>
|
>
|
||||||
</div>
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu Dropdown -->
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
<div
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
id="mobile-menu"
|
||||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
>
|
||||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
|
|||||||
+82
-42
@@ -11,53 +11,93 @@
|
|||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl ml-2"> <a href="index.html">BentoPDF</a> </span>
|
id="home-logo"
|
||||||
</div>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Mobile Hamburger Button -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="md:hidden flex items-center">
|
||||||
<button id="mobile-menu-button" type="button"
|
<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"
|
id="mobile-menu-button"
|
||||||
aria-controls="mobile-menu" aria-expanded="false">
|
type="button"
|
||||||
<span class="sr-only">Open main menu</span>
|
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"
|
||||||
<!-- Hamburger Icon -->
|
aria-controls="mobile-menu"
|
||||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
aria-expanded="false"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<span class="sr-only">Open main menu</span>
|
||||||
d="M4 6h16M4 12h16M4 18h16" />
|
<!-- Hamburger Icon -->
|
||||||
</svg>
|
<svg
|
||||||
<!-- Close Icon -->
|
id="menu-icon"
|
||||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
class="block h-6 w-6"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
fill="none"
|
||||||
d="M6 18L18 6M6 6l12 12" />
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
aria-hidden="true"
|
||||||
</div>
|
>
|
||||||
</div>
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu Dropdown -->
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
<div
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
id="mobile-menu"
|
||||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
>
|
||||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
|
|||||||
+3
-3
@@ -1,9 +1,9 @@
|
|||||||
services:
|
services:
|
||||||
bentopdf:
|
bentopdf:
|
||||||
image: bentopdf/bentopdf:latest
|
# simple mode - bentopdf/bentopdf-simple:latest
|
||||||
|
# default mode - bentopdf/bentopdf:latest
|
||||||
|
image: bentopdf/bentopdf-simple:latest
|
||||||
container_name: bentopdf
|
container_name: bentopdf
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- '3000:80'
|
- '3000:80'
|
||||||
environment:
|
|
||||||
- SIMPLE_MODE=${SIMPLE_MODE:-false}
|
|
||||||
@@ -14,53 +14,93 @@
|
|||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl ml-2"> <a href="index.html">BentoPDF</a> </span>
|
id="home-logo"
|
||||||
</div>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Mobile Hamburger Button -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="md:hidden flex items-center">
|
||||||
<button id="mobile-menu-button" type="button"
|
<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"
|
id="mobile-menu-button"
|
||||||
aria-controls="mobile-menu" aria-expanded="false">
|
type="button"
|
||||||
<span class="sr-only">Open main menu</span>
|
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"
|
||||||
<!-- Hamburger Icon -->
|
aria-controls="mobile-menu"
|
||||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
aria-expanded="false"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<span class="sr-only">Open main menu</span>
|
||||||
d="M4 6h16M4 12h16M4 18h16" />
|
<!-- Hamburger Icon -->
|
||||||
</svg>
|
<svg
|
||||||
<!-- Close Icon -->
|
id="menu-icon"
|
||||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
class="block h-6 w-6"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
fill="none"
|
||||||
d="M6 18L18 6M6 6l12 12" />
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
aria-hidden="true"
|
||||||
</div>
|
>
|
||||||
</div>
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu Dropdown -->
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
<div
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
id="mobile-menu"
|
||||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
>
|
||||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
|
|||||||
+82
-64
@@ -4,81 +4,99 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>BentoPDF - The Privacy First PDF Toolkit</title>
|
<title>BentoPDF - The Privacy First PDF Toolkit</title>
|
||||||
<script
|
|
||||||
async
|
|
||||||
src="https://scripts.simpleanalyticscdn.com/latest.js"
|
|
||||||
></script>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.css"
|
|
||||||
/>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400..700&family=Great+Vibes&family=Kalam:wght@300;400;700&family=Lato:ital,wght@0,400;0,700;1,400&family=Merriweather:ital,wght@0,400;0,700;1,400&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<link rel="icon" type="image/png" href="./images/favicon.svg" />
|
<link rel="icon" type="image/png" href="./images/favicon.svg" />
|
||||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="antialiased">
|
<body class="antialiased">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl ml-2"> <a href="index.html">BentoPDF</a> </span>
|
id="home-logo"
|
||||||
</div>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Mobile Hamburger Button -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="md:hidden flex items-center">
|
||||||
<button id="mobile-menu-button" type="button"
|
<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"
|
id="mobile-menu-button"
|
||||||
aria-controls="mobile-menu" aria-expanded="false">
|
type="button"
|
||||||
<span class="sr-only">Open main menu</span>
|
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"
|
||||||
<!-- Hamburger Icon -->
|
aria-controls="mobile-menu"
|
||||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
aria-expanded="false"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<span class="sr-only">Open main menu</span>
|
||||||
d="M4 6h16M4 12h16M4 18h16" />
|
<!-- Hamburger Icon -->
|
||||||
</svg>
|
<svg
|
||||||
<!-- Close Icon -->
|
id="menu-icon"
|
||||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
class="block h-6 w-6"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
fill="none"
|
||||||
d="M6 18L18 6M6 6l12 12" />
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
aria-hidden="true"
|
||||||
</div>
|
>
|
||||||
</div>
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu Dropdown -->
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
<div
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
id="mobile-menu"
|
||||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
>
|
||||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
<section id="hero-section" class="text-center py-20">
|
<section id="hero-section" class="text-center py-20">
|
||||||
|
|||||||
Generated
+288
-221
@@ -1,16 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"version": "0.0.0",
|
"version": "1.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"version": "0.0.0",
|
"version": "1.1.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||||
|
"@fontsource/dancing-script": "^5.2.8",
|
||||||
|
"@fontsource/dm-sans": "^5.2.8",
|
||||||
|
"@fontsource/great-vibes": "^5.2.8",
|
||||||
|
"@fontsource/kalam": "^5.2.8",
|
||||||
|
"@fontsource/lato": "^5.2.7",
|
||||||
|
"@fontsource/merriweather": "^5.2.11",
|
||||||
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.15",
|
||||||
"blob-stream": "^0.1.3",
|
"blob-stream": "^0.1.3",
|
||||||
"cropperjs": "^1.6.1",
|
"cropperjs": "^1.6.1",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
@@ -18,7 +25,7 @@
|
|||||||
"javascript-obfuscator": "^4.1.1",
|
"javascript-obfuscator": "^4.1.1",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.3",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide": "^0.545.0",
|
"lucide": "^0.546.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^5.4.296",
|
"pdfjs-dist": "^5.4.296",
|
||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
@@ -32,17 +39,17 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@types/blob-stream": "^0.1.33",
|
"@types/blob-stream": "^0.1.33",
|
||||||
"@types/html2canvas": "^0.5.35",
|
"@types/html2canvas": "^1.0.0",
|
||||||
"@types/pdfkit": "^0.17.3",
|
"@types/pdfkit": "^0.17.3",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@types/utif": "^3.0.6",
|
"@types/utif": "^3.0.6",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"ts-migrate": "^0.1.35",
|
"ts-migrate": "^0.1.35",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.11",
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
@@ -86,9 +93,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@asamuzakjp/dom-selector": {
|
"node_modules/@asamuzakjp/dom-selector": {
|
||||||
"version": "6.6.2",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.2.tgz",
|
||||||
"integrity": "sha512-+AG0jN9HTwfDLBhjhX1FKi6zlIAc/YGgEHlN/OMaHD1pOPFsC5CpYQpLkPX0aFjyaVmoq9330cQDCU4qnSL1qA==",
|
"integrity": "sha512-ccKogJI+0aiDhOahdjANIc9SDixSud1gbwdVrhn7kMopAtLXqsz9MKmQQtIl6Y5aC2IYq+j4dz/oedL2AVMmVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2576,6 +2583,69 @@
|
|||||||
"node": "^10.12.0 || >=12.0.0"
|
"node": "^10.12.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource/cedarville-cursive": {
|
||||||
|
"version": "5.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/cedarville-cursive/-/cedarville-cursive-5.2.7.tgz",
|
||||||
|
"integrity": "sha512-G1Wcc8U9MsFV7t4vmxSaWEDVcJj86qbkuS/V27S5UngxkX6CoTpEdvDeGUIKBlF8FoHCDOQxSNZCFuCafRmvUg==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/dancing-script": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/dancing-script/-/dancing-script-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-zHVVgIQ5/rAIM0VVp1QMsqvJm9INHO5/C82E9rHT6NlabjaPTFYn8wl9lT4vPWizfB0TublGQM1999kfdA+XFA==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/dm-sans": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/dm-sans/-/dm-sans-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-tlovG42m9ESG28WiHpLq3F5umAlm64rv0RkqTbYowRn70e9OlRr5a3yTJhrhrY+k5lftR/OFJjPzOLQzk8EfCA==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/great-vibes": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/great-vibes/-/great-vibes-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-/PATz436NGixjNaigierUpv50wfNv2UhAAijcCPkn4ynvl41x66TCLbXVSVfp4pth9gjWnGqgoj0lVIhHapAuw==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/kalam": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/kalam/-/kalam-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-9WDYPX1llS28xRjrqAoz3IOLeaBhCtL3HCtNanYnF1fthFPmSD5aESL6s7C1SJxAlxq/qzTlpIpw4S1W9+q3OA==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/lato": {
|
||||||
|
"version": "5.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/lato/-/lato-5.2.7.tgz",
|
||||||
|
"integrity": "sha512-k5mum1ADbDW5cTw1Ett1eQVWeoZ6gq0ct6SFBibEhB4LRxhniChJZTBgd6ph5yBxLkN1fcnsnmicBNA4S/3nbw==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/merriweather": {
|
||||||
|
"version": "5.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/merriweather/-/merriweather-5.2.11.tgz",
|
||||||
|
"integrity": "sha512-ZiIMeUh5iT8d73o6xlSF8GKgjV5pgiFrufYc5jZTVAfExtWKqM2vQHnsqXSFMv4ELhAcjt6Vf+5T3oVGXhAizQ==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||||
@@ -2703,18 +2773,6 @@
|
|||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/fs-minipass": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"minipass": "^7.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@istanbuljs/schema": {
|
"node_modules/@istanbuljs/schema": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||||
@@ -3420,52 +3478,47 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz",
|
||||||
"integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==",
|
"integrity": "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
"enhanced-resolve": "^5.18.3",
|
"enhanced-resolve": "^5.18.3",
|
||||||
"jiti": "^2.6.0",
|
"jiti": "^2.6.0",
|
||||||
"lightningcss": "1.30.1",
|
"lightningcss": "1.30.2",
|
||||||
"magic-string": "^0.30.19",
|
"magic-string": "^0.30.19",
|
||||||
"source-map-js": "^1.2.1",
|
"source-map-js": "^1.2.1",
|
||||||
"tailwindcss": "4.1.14"
|
"tailwindcss": "4.1.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide": {
|
"node_modules/@tailwindcss/oxide": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz",
|
||||||
"integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==",
|
"integrity": "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==",
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"detect-libc": "^2.0.4",
|
|
||||||
"tar": "^7.5.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tailwindcss/oxide-android-arm64": "4.1.14",
|
"@tailwindcss/oxide-android-arm64": "4.1.15",
|
||||||
"@tailwindcss/oxide-darwin-arm64": "4.1.14",
|
"@tailwindcss/oxide-darwin-arm64": "4.1.15",
|
||||||
"@tailwindcss/oxide-darwin-x64": "4.1.14",
|
"@tailwindcss/oxide-darwin-x64": "4.1.15",
|
||||||
"@tailwindcss/oxide-freebsd-x64": "4.1.14",
|
"@tailwindcss/oxide-freebsd-x64": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14",
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.14",
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.14",
|
"@tailwindcss/oxide-linux-arm64-musl": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.14",
|
"@tailwindcss/oxide-linux-x64-gnu": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.14",
|
"@tailwindcss/oxide-linux-x64-musl": "4.1.15",
|
||||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.14",
|
"@tailwindcss/oxide-wasm32-wasi": "4.1.15",
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.14",
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.15",
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.14"
|
"@tailwindcss/oxide-win32-x64-msvc": "4.1.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.15.tgz",
|
||||||
"integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==",
|
"integrity": "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3479,9 +3532,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.15.tgz",
|
||||||
"integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==",
|
"integrity": "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3495,9 +3548,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.15.tgz",
|
||||||
"integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==",
|
"integrity": "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3511,9 +3564,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.15.tgz",
|
||||||
"integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==",
|
"integrity": "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3527,9 +3580,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.15.tgz",
|
||||||
"integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==",
|
"integrity": "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -3543,9 +3596,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.15.tgz",
|
||||||
"integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==",
|
"integrity": "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3559,9 +3612,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.15.tgz",
|
||||||
"integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==",
|
"integrity": "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3575,9 +3628,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.15.tgz",
|
||||||
"integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==",
|
"integrity": "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3591,9 +3644,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.15.tgz",
|
||||||
"integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==",
|
"integrity": "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3607,9 +3660,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.15.tgz",
|
||||||
"integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==",
|
"integrity": "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==",
|
||||||
"bundleDependencies": [
|
"bundleDependencies": [
|
||||||
"@napi-rs/wasm-runtime",
|
"@napi-rs/wasm-runtime",
|
||||||
"@emnapi/core",
|
"@emnapi/core",
|
||||||
@@ -3627,7 +3680,7 @@
|
|||||||
"@emnapi/core": "^1.5.0",
|
"@emnapi/core": "^1.5.0",
|
||||||
"@emnapi/runtime": "^1.5.0",
|
"@emnapi/runtime": "^1.5.0",
|
||||||
"@emnapi/wasi-threads": "^1.1.0",
|
"@emnapi/wasi-threads": "^1.1.0",
|
||||||
"@napi-rs/wasm-runtime": "^1.0.5",
|
"@napi-rs/wasm-runtime": "^1.0.7",
|
||||||
"@tybys/wasm-util": "^0.10.1",
|
"@tybys/wasm-util": "^0.10.1",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
@@ -3635,10 +3688,64 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.1.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "^1.5.0",
|
||||||
|
"@emnapi/runtime": "^1.5.0",
|
||||||
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz",
|
||||||
"integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==",
|
"integrity": "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3652,9 +3759,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.15.tgz",
|
||||||
"integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==",
|
"integrity": "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3668,14 +3775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/vite": {
|
"node_modules/@tailwindcss/vite": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.15.tgz",
|
||||||
"integrity": "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==",
|
"integrity": "sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/node": "4.1.14",
|
"@tailwindcss/node": "4.1.15",
|
||||||
"@tailwindcss/oxide": "4.1.14",
|
"@tailwindcss/oxide": "4.1.15",
|
||||||
"tailwindcss": "4.1.14"
|
"tailwindcss": "4.1.15"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
@@ -3801,23 +3908,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/html2canvas": {
|
"node_modules/@types/html2canvas": {
|
||||||
"version": "0.5.35",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-0.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz",
|
||||||
"integrity": "sha512-1A2dtWZbOIZ+rUK8jmAx1We/EiNV+5vScpphU3AF14Vby6COIazi/9StosrvlVCqlQegRhsEgZf7QYOuWbwuuA==",
|
"integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==",
|
||||||
|
"deprecated": "This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed.",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/jquery": "*"
|
"html2canvas": "*"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/jquery": {
|
|
||||||
"version": "3.5.33",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz",
|
|
||||||
"integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/sizzle": "*"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/minimatch": {
|
"node_modules/@types/minimatch": {
|
||||||
@@ -3859,13 +3957,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/sizzle": {
|
|
||||||
"version": "2.3.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz",
|
|
||||||
"integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/sortablejs": {
|
"node_modules/@types/sortablejs": {
|
||||||
"version": "1.15.8",
|
"version": "1.15.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
|
||||||
@@ -5046,15 +5137,6 @@
|
|||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chownr": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cipher-base": {
|
"node_modules/cipher-base": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
|
||||||
@@ -8088,21 +8170,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsdom": {
|
"node_modules/jsdom": {
|
||||||
"version": "27.0.0",
|
"version": "27.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz",
|
||||||
"integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==",
|
"integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asamuzakjp/dom-selector": "^6.5.4",
|
"@asamuzakjp/dom-selector": "^6.7.2",
|
||||||
"cssstyle": "^5.3.0",
|
"cssstyle": "^5.3.1",
|
||||||
"data-urls": "^6.0.0",
|
"data-urls": "^6.0.0",
|
||||||
"decimal.js": "^10.5.0",
|
"decimal.js": "^10.6.0",
|
||||||
"html-encoding-sniffer": "^4.0.0",
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
"http-proxy-agent": "^7.0.2",
|
"http-proxy-agent": "^7.0.2",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
"is-potential-custom-element-name": "^1.0.1",
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
"parse5": "^7.3.0",
|
"parse5": "^8.0.0",
|
||||||
"rrweb-cssom": "^0.8.0",
|
"rrweb-cssom": "^0.8.0",
|
||||||
"saxes": "^6.0.0",
|
"saxes": "^6.0.0",
|
||||||
"symbol-tree": "^3.2.4",
|
"symbol-tree": "^3.2.4",
|
||||||
@@ -8111,8 +8193,8 @@
|
|||||||
"webidl-conversions": "^8.0.0",
|
"webidl-conversions": "^8.0.0",
|
||||||
"whatwg-encoding": "^3.1.1",
|
"whatwg-encoding": "^3.1.1",
|
||||||
"whatwg-mimetype": "^4.0.0",
|
"whatwg-mimetype": "^4.0.0",
|
||||||
"whatwg-url": "^15.0.0",
|
"whatwg-url": "^15.1.0",
|
||||||
"ws": "^8.18.2",
|
"ws": "^8.18.3",
|
||||||
"xml-name-validator": "^5.0.0"
|
"xml-name-validator": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -8306,9 +8388,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
@@ -8321,22 +8403,43 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"lightningcss-darwin-arm64": "1.30.1",
|
"lightningcss-android-arm64": "1.30.2",
|
||||||
"lightningcss-darwin-x64": "1.30.1",
|
"lightningcss-darwin-arm64": "1.30.2",
|
||||||
"lightningcss-freebsd-x64": "1.30.1",
|
"lightningcss-darwin-x64": "1.30.2",
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.30.1",
|
"lightningcss-freebsd-x64": "1.30.2",
|
||||||
"lightningcss-linux-arm64-gnu": "1.30.1",
|
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||||
"lightningcss-linux-arm64-musl": "1.30.1",
|
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||||
"lightningcss-linux-x64-gnu": "1.30.1",
|
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||||
"lightningcss-linux-x64-musl": "1.30.1",
|
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||||
"lightningcss-win32-arm64-msvc": "1.30.1",
|
"lightningcss-linux-x64-musl": "1.30.2",
|
||||||
"lightningcss-win32-x64-msvc": "1.30.1"
|
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-android-arm64": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||||
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
|
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8354,9 +8457,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||||
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
|
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8374,9 +8477,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-freebsd-x64": {
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||||
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
|
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8394,9 +8497,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||||
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
|
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -8414,9 +8517,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||||
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
|
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8434,9 +8537,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||||
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
|
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8454,9 +8557,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||||
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
|
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8474,9 +8577,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||||
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
|
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8494,9 +8597,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||||
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
|
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8514,9 +8617,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||||
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
|
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8630,9 +8733,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/lucide": {
|
"node_modules/lucide": {
|
||||||
"version": "0.545.0",
|
"version": "0.546.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide/-/lucide-0.545.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide/-/lucide-0.546.0.tgz",
|
||||||
"integrity": "sha512-mrBH0upkb1TH8ZLkf0XERQAKMdTJ1C+Offr7eSvanmdQ7JrV8jkrFw5jgRAS8fygiZgG4X9mfEg/BXCL+mNMRw==",
|
"integrity": "sha512-YJES3MM/naQS4wJ0JLzTY3anooqWw5iTsPCffHbSMncdxJT2C5tmkCDAwIHMHG8TMtaQcu40KREMPss2qH/+yA==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
@@ -8855,23 +8958,12 @@
|
|||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minizlib": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"minipass": "^7.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mixin-deep": {
|
"node_modules/mixin-deep": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||||
@@ -9470,9 +9562,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/parse5": {
|
"node_modules/parse5": {
|
||||||
"version": "7.3.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
||||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -11263,9 +11355,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
||||||
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
|
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
@@ -11281,22 +11373,6 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
|
||||||
"version": "7.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
|
|
||||||
"integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@isaacs/fs-minipass": "^4.0.0",
|
|
||||||
"chownr": "^3.0.0",
|
|
||||||
"minipass": "^7.1.2",
|
|
||||||
"minizlib": "^3.1.0",
|
|
||||||
"yallist": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/temp": {
|
"node_modules/temp": {
|
||||||
"version": "0.8.4",
|
"version": "0.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz",
|
||||||
@@ -12256,9 +12332,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.9",
|
"version": "7.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
||||||
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
|
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
@@ -12771,15 +12847,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/yallist": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "15.4.1",
|
"version": "15.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
|||||||
+15
-7
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.1",
|
"version": "1.1.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -18,28 +18,36 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"release": "node scripts/release.js patch",
|
"release": "node scripts/release.js patch",
|
||||||
"release:minor": "node scripts/release.js minor",
|
"release:minor": "node scripts/release.js minor",
|
||||||
"release:major": "node scripts/release.js major"
|
"release:major": "node scripts/release.js major",
|
||||||
|
"serve:simple": "SIMPLE_MODE=true npm run build && npx serve dist -p 3000"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@types/blob-stream": "^0.1.33",
|
"@types/blob-stream": "^0.1.33",
|
||||||
"@types/html2canvas": "^0.5.35",
|
"@types/html2canvas": "^1.0.0",
|
||||||
"@types/pdfkit": "^0.17.3",
|
"@types/pdfkit": "^0.17.3",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@types/utif": "^3.0.6",
|
"@types/utif": "^3.0.6",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.1",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"ts-migrate": "^0.1.35",
|
"ts-migrate": "^0.1.35",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.11",
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||||
|
"@fontsource/dancing-script": "^5.2.8",
|
||||||
|
"@fontsource/dm-sans": "^5.2.8",
|
||||||
|
"@fontsource/great-vibes": "^5.2.8",
|
||||||
|
"@fontsource/kalam": "^5.2.8",
|
||||||
|
"@fontsource/lato": "^5.2.7",
|
||||||
|
"@fontsource/merriweather": "^5.2.11",
|
||||||
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.15",
|
||||||
"blob-stream": "^0.1.3",
|
"blob-stream": "^0.1.3",
|
||||||
"cropperjs": "^1.6.1",
|
"cropperjs": "^1.6.1",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
@@ -47,7 +55,7 @@
|
|||||||
"javascript-obfuscator": "^4.1.1",
|
"javascript-obfuscator": "^4.1.1",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.3",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide": "^0.545.0",
|
"lucide": "^0.546.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^5.4.296",
|
"pdfjs-dist": "^5.4.296",
|
||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
|
|||||||
+82
-42
@@ -13,53 +13,93 @@
|
|||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl ml-2"> <a href="index.html">BentoPDF</a> </span>
|
id="home-logo"
|
||||||
</div>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Mobile Hamburger Button -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="md:hidden flex items-center">
|
||||||
<button id="mobile-menu-button" type="button"
|
<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"
|
id="mobile-menu-button"
|
||||||
aria-controls="mobile-menu" aria-expanded="false">
|
type="button"
|
||||||
<span class="sr-only">Open main menu</span>
|
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"
|
||||||
<!-- Hamburger Icon -->
|
aria-controls="mobile-menu"
|
||||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
aria-expanded="false"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<span class="sr-only">Open main menu</span>
|
||||||
d="M4 6h16M4 12h16M4 18h16" />
|
<!-- Hamburger Icon -->
|
||||||
</svg>
|
<svg
|
||||||
<!-- Close Icon -->
|
id="menu-icon"
|
||||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
class="block h-6 w-6"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
fill="none"
|
||||||
d="M6 18L18 6M6 6l12 12" />
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
aria-hidden="true"
|
||||||
</div>
|
>
|
||||||
</div>
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu Dropdown -->
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
<div
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
id="mobile-menu"
|
||||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
>
|
||||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="container mx-auto p-4 md:p-8">
|
<div id="app" class="container mx-auto p-4 md:p-8">
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
### Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||||
|
|
||||||
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
### [v1.0.0](https://github.com/alam00000/bentopdf/compare/v0.0.1...v1.0.0)
|
||||||
|
|
||||||
|
> 20 October 2025
|
||||||
|
|
||||||
|
- feat(ocr): add whitelist presets and improve UI for OCR tool [`0e7c535`](https://github.com/alam00000/bentopdf/commit/0e7c53560a23d3e26a18904aa47ca99d3f10d9a6)
|
||||||
|
- Release v1.0.0 [`6ad6a7d`](https://github.com/alam00000/bentopdf/commit/6ad6a7d7305eb77fe9ebdb55d93059bf92304a98)
|
||||||
|
|
||||||
|
#### v0.0.1
|
||||||
|
|
||||||
|
> 20 October 2025
|
||||||
|
|
||||||
|
- feat: add Simple Mode for internal organizational use [`#64`](https://github.com/alam00000/bentopdf/pull/64)
|
||||||
|
- Feature/reverse multi pdfs [`#58`](https://github.com/alam00000/bentopdf/pull/58)
|
||||||
|
- added description to Unraid XML [`#65`](https://github.com/alam00000/bentopdf/pull/65)
|
||||||
|
- Added hamburger Navbar [`#62`](https://github.com/alam00000/bentopdf/pull/62)
|
||||||
|
- feat(versioning): Added version management sysytem [`#52`](https://github.com/alam00000/bentopdf/pull/52)
|
||||||
|
- Fix#20 : fixed the tesseract issue [`#60`](https://github.com/alam00000/bentopdf/pull/60)
|
||||||
|
- feat: add search bar keyboard shortcut [`#49`](https://github.com/alam00000/bentopdf/pull/49)
|
||||||
|
- feat(pdf-tools): add attachments feature to embed files in PDFs [`#53`](https://github.com/alam00000/bentopdf/pull/53)
|
||||||
|
- feat(pdf-tools): add linearize pdf tool for web optimization [`#51`](https://github.com/alam00000/bentopdf/pull/51)
|
||||||
|
- feat: add support for multiple blank pages insertion [`#42`](https://github.com/alam00000/bentopdf/pull/42)
|
||||||
|
- Multiupload handle encrypted pdf [`#43`](https://github.com/alam00000/bentopdf/pull/43)
|
||||||
|
- feat:Setup Prettier for code formatting [`#30`](https://github.com/alam00000/bentopdf/pull/30)
|
||||||
|
- created png as unraid does not supprot svg [`#24`](https://github.com/alam00000/bentopdf/pull/24)
|
||||||
|
- adding unraid template [`#23`](https://github.com/alam00000/bentopdf/pull/23)
|
||||||
|
- feat(pdf-tools): add alternate merge tool for mixing pdf pages [`#21`](https://github.com/alam00000/bentopdf/pull/21)
|
||||||
|
- feat: add remove blank pages tool with sensitivity control [`#19`](https://github.com/alam00000/bentopdf/pull/19)
|
||||||
|
- Posterize [`#17`](https://github.com/alam00000/bentopdf/pull/17)
|
||||||
|
- feat: cicd for build and push to docker registry [`#15`](https://github.com/alam00000/bentopdf/pull/15)
|
||||||
|
- Fix(posterize): Resolve incorrect vertical tiling/cropping [`#10`](https://github.com/alam00000/bentopdf/pull/10)
|
||||||
|
- feat: add initial project setup with core PDF tools and utilities [`6712973`](https://github.com/alam00000/bentopdf/commit/671297320ec6ff93d05ad150836dd0a2b291c83c)
|
||||||
|
- feat(test): add comprehensive test suite and testing infrastructure [`5e72d7c`](https://github.com/alam00000/bentopdf/commit/5e72d7ca2debe62fc3e74606b967cdb8334ce827)
|
||||||
|
- refactor: fix type errors and improve type safety in PDF logic [`cc83d79`](https://github.com/alam00000/bentopdf/commit/cc83d7951f8748bb3b40223fc1ffd5d397faad72)
|
||||||
+23
-16
@@ -18,7 +18,7 @@ function getCurrentVersion() {
|
|||||||
function updateVersion(type) {
|
function updateVersion(type) {
|
||||||
const currentVersion = getCurrentVersion();
|
const currentVersion = getCurrentVersion();
|
||||||
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||||
|
|
||||||
let newVersion;
|
let newVersion;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'major':
|
case 'major':
|
||||||
@@ -32,23 +32,30 @@ function updateVersion(type) {
|
|||||||
newVersion = `${major}.${minor}.${patch + 1}`;
|
newVersion = `${major}.${minor}.${patch + 1}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
packageJson.version = newVersion;
|
packageJson.version = newVersion;
|
||||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
fs.writeFileSync(
|
||||||
|
packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2) + '\n'
|
||||||
|
);
|
||||||
return newVersion;
|
return newVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createGitTag(version) {
|
function createGitTag(version) {
|
||||||
const tagName = `v${version}`;
|
const tagName = `v${version}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if tag already exists
|
// Check if tag already exists
|
||||||
execSync(`git rev-parse "v${version}" >/dev/null 2>&1`, { stdio: 'ignore' });
|
execSync(`git rev-parse "v${version}" >/dev/null 2>&1`, {
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
console.log(`✅ Tag v${version} already exists`);
|
console.log(`✅ Tag v${version} already exists`);
|
||||||
return tagName;
|
return tagName;
|
||||||
} catch {
|
} catch {
|
||||||
// Tag doesn't exist, create it
|
// Tag doesn't exist, create it
|
||||||
execSync(`git tag -a "v${version}" -m "Release v${version}"`, { stdio: 'inherit' });
|
execSync(`git tag -a "v${version}" -m "Release v${version}"`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
console.log(`✅ Created tag v${version}`);
|
console.log(`✅ Created tag v${version}`);
|
||||||
return tagName;
|
return tagName;
|
||||||
}
|
}
|
||||||
@@ -56,36 +63,36 @@ function createGitTag(version) {
|
|||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const type = process.argv[2] || 'patch';
|
const type = process.argv[2] || 'patch';
|
||||||
|
|
||||||
if (!['major', 'minor', 'patch'].includes(type)) {
|
if (!['major', 'minor', 'patch'].includes(type)) {
|
||||||
console.error('❌ Invalid version type. Use: major, minor, or patch');
|
console.error('❌ Invalid version type. Use: major, minor, or patch');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🚀 Releasing ${type} version...`);
|
console.log(`🚀 Releasing ${type} version...`);
|
||||||
|
|
||||||
// 1. Update version in package.json
|
// 1. Update version in package.json
|
||||||
const newVersion = updateVersion(type);
|
const newVersion = updateVersion(type);
|
||||||
console.log(`📦 Updated version to ${newVersion}`);
|
console.log(`📦 Updated version to ${newVersion}`);
|
||||||
|
|
||||||
// 2. Add and commit changes
|
// 2. Add and commit changes
|
||||||
execSync('git add package.json', { stdio: 'inherit' });
|
execSync('git add package.json', { stdio: 'inherit' });
|
||||||
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
|
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
|
||||||
console.log(`💾 Committed version change`);
|
console.log(`💾 Committed version change`);
|
||||||
|
|
||||||
// 3. Create git tag
|
// 3. Create git tag
|
||||||
const tagName = createGitTag(newVersion);
|
const tagName = createGitTag(newVersion);
|
||||||
|
|
||||||
// 4. Push everything to main
|
// 4. Push everything to main
|
||||||
console.log(`📤 Pushing to main...`);
|
console.log(`📤 Pushing to main...`);
|
||||||
execSync('git push origin main', { stdio: 'inherit' });
|
execSync('git push origin main', { stdio: 'inherit' });
|
||||||
execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
|
execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
|
||||||
|
|
||||||
console.log(`🎉 Release v${newVersion} complete!`);
|
console.log(`🎉 Release v${newVersion} complete!`);
|
||||||
console.log(`📦 Docker image: bentopdf/bentopdf:${newVersion}`);
|
console.log(`📦 Docker image: bentopdf/bentopdf:${newVersion}`);
|
||||||
console.log(`🏷️ GitHub release: https://github.com/AltuisticIsopod/bentopdf/releases/tag/${tagName}`);
|
console.log(
|
||||||
|
`🏷️ GitHub release: https://github.com/AltuisticIsopod/bentopdf/releases/tag/${tagName}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,49 @@
|
|||||||
|
@import '@fontsource/dm-sans/400.css';
|
||||||
|
@import '@fontsource/dm-sans/500.css';
|
||||||
|
@import '@fontsource/dm-sans/600.css';
|
||||||
|
@import '@fontsource/dm-sans/700.css';
|
||||||
|
@import '@fontsource/dancing-script/400.css';
|
||||||
|
@import '@fontsource/great-vibes/400.css';
|
||||||
|
@import '@fontsource/kalam/300.css';
|
||||||
|
@import '@fontsource/kalam/400.css';
|
||||||
|
@import '@fontsource/kalam/700.css';
|
||||||
|
@import '@fontsource/lato/400.css';
|
||||||
|
@import '@fontsource/lato/700.css';
|
||||||
|
@import '@fontsource/lato/400-italic.css';
|
||||||
|
@import '@fontsource/merriweather/400.css';
|
||||||
|
@import '@fontsource/merriweather/700.css';
|
||||||
|
@import '@fontsource/merriweather/400-italic.css';
|
||||||
|
@import '@fontsource/cedarville-cursive/400.css';
|
||||||
|
@import 'cropperjs/dist/cropper.css';
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-dancing: 'Dancing Script', cursive;
|
||||||
|
--font-vibes: 'Great Vibes', cursive;
|
||||||
|
--font-kalam: 'Kalam', cursive;
|
||||||
|
--font-lato: 'Lato', sans-serif;
|
||||||
|
--font-merriweather: 'Merriweather', serif;
|
||||||
|
--font-cedarville: 'Cedarville Cursive', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply antialiased;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: 'DM Sans', ui-sans-serif, system-ui;
|
||||||
|
--font-dancing: 'Dancing Script', cursive;
|
||||||
|
--font-vibes: 'Great Vibes', cursive;
|
||||||
|
--font-kalam: 'Kalam', cursive;
|
||||||
|
--font-lato: 'Lato', sans-serif;
|
||||||
|
--font-merriweather: 'Merriweather', serif;
|
||||||
|
--font-cedarville: 'Cedarville Cursive', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ export const singlePdfLoadTools = [
|
|||||||
'posterize',
|
'posterize',
|
||||||
'remove-blank-pages',
|
'remove-blank-pages',
|
||||||
'add-attachments',
|
'add-attachments',
|
||||||
|
'sanitize-pdf',
|
||||||
|
'remove-restrictions',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const simpleTools = [
|
export const simpleTools = [
|
||||||
|
|||||||
+17
-4
@@ -369,7 +369,7 @@ export const categories = [
|
|||||||
{
|
{
|
||||||
id: 'posterize',
|
id: 'posterize',
|
||||||
name: 'Posterize PDF',
|
name: 'Posterize PDF',
|
||||||
icon: 'layout-grid',
|
icon: 'notepad-text-dashed',
|
||||||
subtitle: 'Split a large page into multiple smaller pages.',
|
subtitle: 'Split a large page into multiple smaller pages.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -392,7 +392,7 @@ export const categories = [
|
|||||||
{
|
{
|
||||||
id: 'linearize',
|
id: 'linearize',
|
||||||
name: 'Linearize PDF',
|
name: 'Linearize PDF',
|
||||||
icon: 'zap',
|
icon: 'gauge',
|
||||||
subtitle: 'Optimize PDF for fast web viewing.',
|
subtitle: 'Optimize PDF for fast web viewing.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -401,6 +401,13 @@ export const categories = [
|
|||||||
icon: 'ruler',
|
icon: 'ruler',
|
||||||
subtitle: 'Analyze page size, orientation, and units.',
|
subtitle: 'Analyze page size, orientation, and units.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-restrictions',
|
||||||
|
name: 'Remove Restrictions',
|
||||||
|
icon: 'unlink',
|
||||||
|
subtitle:
|
||||||
|
'Remove password protection and security restrictions associated with digitally signed PDF files.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -410,13 +417,19 @@ export const categories = [
|
|||||||
id: 'encrypt',
|
id: 'encrypt',
|
||||||
name: 'Encrypt PDF',
|
name: 'Encrypt PDF',
|
||||||
icon: 'lock',
|
icon: 'lock',
|
||||||
subtitle: 'Add a password to protect your PDF.',
|
subtitle: 'Lock your PDF by adding a password.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sanitize-pdf',
|
||||||
|
name: 'Sanitize PDF',
|
||||||
|
icon: 'brush-cleaning',
|
||||||
|
subtitle: 'Remove metadata, annotations, scripts, and more.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'decrypt',
|
id: 'decrypt',
|
||||||
name: 'Decrypt PDF',
|
name: 'Decrypt PDF',
|
||||||
icon: 'unlock',
|
icon: 'unlock',
|
||||||
subtitle: 'Remove password protection from a PDF.',
|
subtitle: 'Unlock PDF by removing password protection.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'flatten',
|
id: 'flatten',
|
||||||
|
|||||||
+142
-21
@@ -7,7 +7,7 @@ import {
|
|||||||
renderFileDisplay,
|
renderFileDisplay,
|
||||||
switchView,
|
switchView,
|
||||||
} from '../ui.js';
|
} from '../ui.js';
|
||||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { formatIsoDate, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { setupCanvasEditor } from '../canvasEditor.js';
|
import { setupCanvasEditor } from '../canvasEditor.js';
|
||||||
import { toolLogic } from '../logic/index.js';
|
import { toolLogic } from '../logic/index.js';
|
||||||
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
|
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
|
||||||
@@ -25,15 +25,16 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||||
ignoreEncryption: true
|
ignoreEncryption: true,
|
||||||
});
|
});
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
state.pdfDoc.isEncrypted &&
|
state.pdfDoc.isEncrypted &&
|
||||||
toolId !== 'decrypt' &&
|
toolId !== 'decrypt' &&
|
||||||
toolId !== 'change-permissions'
|
toolId !== 'change-permissions' &&
|
||||||
|
toolId !== 'remove-restrictions'
|
||||||
) {
|
) {
|
||||||
showAlert(
|
showAlert(
|
||||||
'Protected PDF',
|
'Protected PDF',
|
||||||
@@ -124,12 +125,13 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const pdfjsDoc = await pdfjsLib.getDocument({
|
const pdfjsDoc = await pdfjsLib.getDocument({
|
||||||
data: pdfBytes as ArrayBuffer,
|
data: pdfBytes as ArrayBuffer,
|
||||||
}).promise;
|
}).promise;
|
||||||
const [metadata, fieldObjects] = await Promise.all([
|
const [metadataResult, fieldObjects] = await Promise.all([
|
||||||
pdfjsDoc.getMetadata(),
|
pdfjsDoc.getMetadata(),
|
||||||
pdfjsDoc.getFieldObjects(),
|
pdfjsDoc.getFieldObjects(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { info, metadata: rawXmpString } = metadata;
|
const { info, metadata } = metadataResult;
|
||||||
|
const rawXmpString = metadata ? metadata.getRaw() : null;
|
||||||
|
|
||||||
resultsDiv.textContent = ''; // Clear safely
|
resultsDiv.textContent = ''; // Clear safely
|
||||||
|
|
||||||
@@ -184,14 +186,29 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const infoSection = createSection('Info Dictionary');
|
const infoSection = createSection('Info Dictionary');
|
||||||
if (info && Object.keys(info).length > 0) {
|
if (info && Object.keys(info).length > 0) {
|
||||||
for (const key in info) {
|
for (const key in info) {
|
||||||
let value = info[key] || '- Not Set -';
|
let value = info[key];
|
||||||
if (
|
let displayValue;
|
||||||
|
|
||||||
|
if (value === null || typeof value === 'undefined') {
|
||||||
|
displayValue = '- Not Set -';
|
||||||
|
} else if (typeof value === 'object' && value.name) {
|
||||||
|
displayValue = value.name;
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
try {
|
||||||
|
displayValue = JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
displayValue = '[object Object]';
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
(key === 'CreationDate' || key === 'ModDate') &&
|
(key === 'CreationDate' || key === 'ModDate') &&
|
||||||
typeof value === 'string'
|
typeof value === 'string'
|
||||||
) {
|
) {
|
||||||
value = parsePdfDate(value);
|
displayValue = parsePdfDate(value);
|
||||||
|
} else {
|
||||||
|
displayValue = String(value);
|
||||||
}
|
}
|
||||||
infoSection.ul.appendChild(createListItem(key, String(value)));
|
|
||||||
|
infoSection.ul.appendChild(createListItem(key, displayValue));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||||
@@ -212,19 +229,119 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
}
|
}
|
||||||
resultsDiv.appendChild(fieldsSection.wrapper);
|
resultsDiv.appendChild(fieldsSection.wrapper);
|
||||||
|
|
||||||
const xmpSection = createSection('XMP Metadata (Raw XML)');
|
const createXmpListItem = (key, value, indent = 0) => {
|
||||||
const xmpContainer = document.createElement('div');
|
const li = document.createElement('li');
|
||||||
xmpContainer.className =
|
li.className = 'flex flex-col sm:flex-row';
|
||||||
'bg-gray-900 p-4 rounded-lg border border-gray-700';
|
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.className = 'w-56 flex-shrink-0 text-gray-400';
|
||||||
|
strong.textContent = key;
|
||||||
|
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex-grow text-white break-all';
|
||||||
|
div.textContent = value;
|
||||||
|
|
||||||
|
li.append(strong, div);
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createXmpHeaderItem = (key, indent = 0) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'flex pt-2';
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium';
|
||||||
|
strong.textContent = key;
|
||||||
|
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
||||||
|
li.append(strong);
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendXmpNodes = (xmlNode, ulElement, indentLevel) => {
|
||||||
|
const xmpDateKeys = [
|
||||||
|
'xap:CreateDate',
|
||||||
|
'xap:ModifyDate',
|
||||||
|
'xap:MetadataDate',
|
||||||
|
];
|
||||||
|
|
||||||
|
const childNodes = Array.from(xmlNode.children);
|
||||||
|
|
||||||
|
for (const child of childNodes) {
|
||||||
|
if ((child as Element).nodeType !== 1) continue;
|
||||||
|
|
||||||
|
let key = (child as Element).tagName;
|
||||||
|
const elementChildren = Array.from(
|
||||||
|
(child as Element).children
|
||||||
|
).filter((c) => c.nodeType === 1);
|
||||||
|
|
||||||
|
if (key === 'rdf:li') {
|
||||||
|
appendXmpNodes(child, ulElement, indentLevel);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === 'rdf:Alt') {
|
||||||
|
key = '(alt container)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(child as Element).getAttribute('rdf:parseType') === 'Resource' &&
|
||||||
|
elementChildren.length === 0
|
||||||
|
) {
|
||||||
|
ulElement.appendChild(
|
||||||
|
createXmpListItem(key, '(Empty Resource)', indentLevel)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementChildren.length > 0) {
|
||||||
|
ulElement.appendChild(createXmpHeaderItem(key, indentLevel));
|
||||||
|
appendXmpNodes(child, ulElement, indentLevel + 1);
|
||||||
|
} else {
|
||||||
|
let value = (child as Element).textContent.trim();
|
||||||
|
if (value) {
|
||||||
|
if (xmpDateKeys.includes(key)) {
|
||||||
|
value = formatIsoDate(value);
|
||||||
|
}
|
||||||
|
ulElement.appendChild(
|
||||||
|
createXmpListItem(key, value, indentLevel)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const xmpSection = createSection('XMP Metadata');
|
||||||
if (rawXmpString) {
|
if (rawXmpString) {
|
||||||
const pre = document.createElement('pre');
|
try {
|
||||||
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
const parser = new DOMParser();
|
||||||
pre.textContent = String(rawXmpString);
|
const xmlDoc = parser.parseFromString(
|
||||||
xmpContainer.appendChild(pre);
|
rawXmpString,
|
||||||
|
'application/xml'
|
||||||
|
);
|
||||||
|
|
||||||
|
const descriptions = xmlDoc.getElementsByTagName('rdf:Description');
|
||||||
|
if (descriptions.length > 0) {
|
||||||
|
for (const desc of descriptions) {
|
||||||
|
appendXmpNodes(desc, xmpSection.ul, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appendXmpNodes(xmlDoc.documentElement, xmpSection.ul, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xmpSection.ul.children.length === 0) {
|
||||||
|
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No parseable XMP properties found -</span></li>`;
|
||||||
|
}
|
||||||
|
} catch (xmlError) {
|
||||||
|
console.error('Failed to parse XMP XML:', xmlError);
|
||||||
|
xmpSection.ul.innerHTML = `<li><span class="text-red-500 italic">- Error parsing XMP XML. Displaying raw. -</span></li>`;
|
||||||
|
const pre = document.createElement('pre');
|
||||||
|
pre.className =
|
||||||
|
'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||||
|
pre.textContent = rawXmpString;
|
||||||
|
xmpSection.ul.appendChild(pre);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No XMP metadata found -</span></li>`;
|
||||||
}
|
}
|
||||||
xmpSection.wrapper.appendChild(xmpContainer);
|
|
||||||
resultsDiv.appendChild(xmpSection.wrapper);
|
resultsDiv.appendChild(xmpSection.wrapper);
|
||||||
|
|
||||||
resultsDiv.classList.remove('hidden');
|
resultsDiv.classList.remove('hidden');
|
||||||
@@ -324,7 +441,11 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleMultiFileUpload(toolId) {
|
async function handleMultiFileUpload(toolId) {
|
||||||
if (toolId === 'merge' || toolId === 'alternate-merge' || toolId === 'reverse-pages') {
|
if (
|
||||||
|
toolId === 'merge' ||
|
||||||
|
toolId === 'alternate-merge' ||
|
||||||
|
toolId === 'reverse-pages'
|
||||||
|
) {
|
||||||
const pdfFilesUnloaded: File[] = [];
|
const pdfFilesUnloaded: File[] = [];
|
||||||
|
|
||||||
state.files.forEach((file) => {
|
state.files.forEach((file) => {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function addAttachments() {
|
|||||||
showAlert('Error', `Failed to attach files: ${error.message}`);
|
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
clearAttachments();
|
clearAttachments();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ export function setupAddAttachmentsTool() {
|
|||||||
|
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate text-sm';
|
nameSpan.className = 'truncate text-sm';
|
||||||
nameSpan.textContent = file.name;
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
const sizeSpan = document.createElement('span');
|
const sizeSpan = document.createElement('span');
|
||||||
sizeSpan.className = 'text-xs text-gray-400';
|
sizeSpan.className = 'text-xs text-gray-400';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
readFileAsArrayBuffer,
|
readFileAsArrayBuffer,
|
||||||
hexToRgb,
|
hexToRgb,
|
||||||
} from '../utils/helpers.js';
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state, resetState } from '../state.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PDFDocument as PDFLibDocument,
|
PDFDocument as PDFLibDocument,
|
||||||
@@ -175,6 +175,15 @@ export async function addWatermark() {
|
|||||||
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
'watermarked.pdf'
|
'watermarked.pdf'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const toolid = state.activeTool;
|
||||||
|
resetState();
|
||||||
|
if (toolid) {
|
||||||
|
const element = document.querySelector(
|
||||||
|
`[data-tool-id="${toolid}"]`
|
||||||
|
) as HTMLElement;
|
||||||
|
if (element) element.click();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert(
|
showAlert(
|
||||||
|
|||||||
+149
-126
@@ -1,146 +1,169 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
initializeQpdf,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
|
||||||
import blobStream from 'blob-stream';
|
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
|
||||||
|
|
||||||
export async function changePermissions() {
|
export async function changePermissions() {
|
||||||
const currentPassword = (
|
const file = state.files[0];
|
||||||
document.getElementById('current-password') as HTMLInputElement
|
const currentPassword =
|
||||||
).value;
|
(document.getElementById('current-password') as HTMLInputElement)?.value ||
|
||||||
const newUserPassword = (
|
'';
|
||||||
document.getElementById('new-user-password') as HTMLInputElement
|
const newUserPassword =
|
||||||
).value;
|
(document.getElementById('new-user-password') as HTMLInputElement)?.value ||
|
||||||
const newOwnerPassword = (
|
'';
|
||||||
document.getElementById('new-owner-password') as HTMLInputElement
|
const newOwnerPassword =
|
||||||
).value;
|
(document.getElementById('new-owner-password') as HTMLInputElement)
|
||||||
|
?.value || '';
|
||||||
|
|
||||||
// An owner password is required to enforce any permissions.
|
const inputPath = '/input.pdf';
|
||||||
if (
|
const outputPath = '/output.pdf';
|
||||||
!newOwnerPassword &&
|
let qpdf: any;
|
||||||
(newUserPassword ||
|
|
||||||
document.querySelectorAll('input[type="checkbox"]:not(:checked)').length >
|
|
||||||
0)
|
|
||||||
) {
|
|
||||||
showAlert(
|
|
||||||
'Input Required',
|
|
||||||
'You must set a "New Owner Password" to enforce specific permissions or to set a user password.'
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Preparing to process...');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = state.files[0];
|
showLoader('Initializing...');
|
||||||
const pdfData = await readFileAsArrayBuffer(file);
|
qpdf = await initializeQpdf();
|
||||||
|
|
||||||
let pdf;
|
showLoader('Reading PDF...');
|
||||||
try {
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
pdf = await pdfjsLib.getDocument({
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
data: pdfData as ArrayBuffer,
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
password: currentPassword,
|
|
||||||
}).promise;
|
showLoader('Processing PDF permissions...');
|
||||||
} catch (e) {
|
|
||||||
// This catch is specific to password errors in pdf.js
|
const args = [inputPath];
|
||||||
if (e.name === 'PasswordException') {
|
|
||||||
hideLoader();
|
// Add password if provided
|
||||||
showAlert(
|
if (currentPassword) {
|
||||||
'Incorrect Password',
|
args.push('--password=' + currentPassword);
|
||||||
'The current password you entered is incorrect.'
|
}
|
||||||
);
|
|
||||||
return;
|
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');
|
||||||
}
|
}
|
||||||
throw e;
|
} else {
|
||||||
|
args.push('--decrypt');
|
||||||
}
|
}
|
||||||
|
|
||||||
const numPages = pdf.numPages;
|
args.push('--', outputPath);
|
||||||
const pageImages = [];
|
try {
|
||||||
|
qpdf.callMain(args);
|
||||||
|
} catch (qpdfError: any) {
|
||||||
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
|
||||||
for (let i = 1; i <= numPages; i++) {
|
const errorMsg = qpdfError.message || '';
|
||||||
document.getElementById('loader-text').textContent =
|
|
||||||
`Processing page ${i} of ${numPages}...`;
|
if (
|
||||||
const page = await pdf.getPage(i);
|
errorMsg.includes('invalid password') ||
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
errorMsg.includes('incorrect password') ||
|
||||||
const canvas = document.createElement('canvas');
|
errorMsg.includes('password')
|
||||||
canvas.height = viewport.height;
|
) {
|
||||||
canvas.width = viewport.width;
|
throw new Error('INVALID_PASSWORD');
|
||||||
const context = canvas.getContext('2d');
|
}
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
|
||||||
pageImages.push({
|
if (
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
errorMsg.includes('encrypted') ||
|
||||||
width: viewport.width,
|
errorMsg.includes('password required')
|
||||||
height: viewport.height,
|
) {
|
||||||
});
|
throw new Error('PASSWORD_REQUIRED');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent =
|
showLoader('Preparing download...');
|
||||||
'Applying new permissions...';
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
|
||||||
const allowPrinting = (
|
if (!outputFile || outputFile.length === 0) {
|
||||||
document.getElementById('allow-printing') as HTMLInputElement
|
throw new Error('Processing resulted in an empty file.');
|
||||||
).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 allowContentAccessibility = (
|
|
||||||
document.getElementById('allow-content-accessibility') as HTMLInputElement
|
|
||||||
).checked;
|
|
||||||
const allowDocumentAssembly = (
|
|
||||||
document.getElementById('allow-document-assembly') as HTMLInputElement
|
|
||||||
).checked;
|
|
||||||
|
|
||||||
const doc = new PDFDocument({
|
|
||||||
size: [pageImages[0].width, pageImages[0].height],
|
|
||||||
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
|
|
||||||
|
|
||||||
// Apply the new, separate user and owner passwords
|
|
||||||
userPassword: newUserPassword,
|
|
||||||
ownerPassword: newOwnerPassword,
|
|
||||||
|
|
||||||
// Apply all seven permissions from the checkboxes
|
|
||||||
permissions: {
|
|
||||||
printing: allowPrinting ? 'highResolution' : false,
|
|
||||||
modifying: allowModifying,
|
|
||||||
copying: allowCopying,
|
|
||||||
annotating: allowAnnotating,
|
|
||||||
fillingForms: allowFillingForms,
|
|
||||||
contentAccessibility: allowContentAccessibility,
|
|
||||||
documentAssembly: allowDocumentAssembly,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const stream = doc.pipe(blobStream());
|
|
||||||
|
|
||||||
for (let i = 0; i < pageImages.length; i++) {
|
|
||||||
if (i > 0)
|
|
||||||
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
|
||||||
doc.image(pageImages[i].data, 0, 0, {
|
|
||||||
width: pageImages[i].width,
|
|
||||||
height: pageImages[i].height,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.end();
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
stream.on('finish', function () {
|
downloadFile(blob, `permissions-changed-${file.name}`);
|
||||||
const blob = stream.toBlob('application/pdf');
|
|
||||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Success', 'Permissions changed successfully!');
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An unexpected error occurred: ${e.message}`);
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+85
-59
@@ -1,79 +1,105 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
initializeQpdf,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
|
||||||
import blobStream from 'blob-stream';
|
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
|
||||||
|
|
||||||
export async function decrypt() {
|
export async function decrypt() {
|
||||||
const file = state.files[0];
|
const file = state.files[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 password = (
|
||||||
const password = document.getElementById('password-input').value;
|
document.getElementById('password-input') as HTMLInputElement
|
||||||
if (!password.trim()) {
|
)?.value;
|
||||||
|
|
||||||
|
if (!password?.trim()) {
|
||||||
showAlert('Input Required', 'Please enter the PDF password.');
|
showAlert('Input Required', 'Please enter the PDF password.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputPath = '/input.pdf';
|
||||||
|
const outputPath = '/output.pdf';
|
||||||
|
let qpdf: any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Preparing to process...');
|
showLoader('Initializing decryption...');
|
||||||
const pdfData = (await readFileAsArrayBuffer(file)) as ArrayBuffer;
|
qpdf = await initializeQpdf();
|
||||||
const pdf = await pdfjsLib.getDocument({
|
|
||||||
data: pdfData,
|
|
||||||
password: password,
|
|
||||||
}).promise;
|
|
||||||
const numPages = pdf.numPages;
|
|
||||||
const pageImages = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= numPages; i++) {
|
showLoader('Reading encrypted PDF...');
|
||||||
document.getElementById('loader-text').textContent =
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
`Processing page ${i} of ${numPages}...`;
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
const page = await pdf.getPage(i);
|
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const context = canvas.getContext('2d');
|
showLoader('Decrypting PDF...');
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
const args = [inputPath, '--password=' + password, '--decrypt', outputPath];
|
||||||
await page.render({
|
|
||||||
canvasContext: context,
|
try {
|
||||||
viewport: viewport,
|
qpdf.callMain(args);
|
||||||
canvas: canvas,
|
} catch (qpdfError: any) {
|
||||||
}).promise;
|
console.error('qpdf execution error:', qpdfError);
|
||||||
pageImages.push({
|
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
if (
|
||||||
width: viewport.width,
|
qpdfError.message?.includes('invalid password') ||
|
||||||
height: viewport.height,
|
qpdfError.message?.includes('password')
|
||||||
});
|
) {
|
||||||
|
throw new Error('INVALID_PASSWORD');
|
||||||
|
}
|
||||||
|
throw qpdfError;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent =
|
showLoader('Preparing download...');
|
||||||
'Building unlocked PDF...';
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
const doc = new PDFDocument({
|
|
||||||
size: [pageImages[0].width, pageImages[0].height],
|
|
||||||
});
|
|
||||||
const stream = doc.pipe(blobStream());
|
|
||||||
for (let i = 0; i < pageImages.length; i++) {
|
|
||||||
if (i > 0)
|
|
||||||
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
|
||||||
doc.image(pageImages[i].data, 0, 0, {
|
|
||||||
width: pageImages[i].width,
|
|
||||||
height: pageImages[i].height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
doc.end();
|
|
||||||
|
|
||||||
stream.on('finish', function () {
|
if (!outputFile || outputFile.length === 0) {
|
||||||
const blob = stream.toBlob('application/pdf');
|
throw new Error('Decryption resulted in an empty file.');
|
||||||
downloadFile(blob, `unlocked-${file.name}`);
|
}
|
||||||
hideLoader();
|
|
||||||
showAlert('Success', 'Decryption complete! Your download has started.');
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
});
|
downloadFile(blob, `unlocked-${file.name}`);
|
||||||
} catch (error) {
|
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'PDF decrypted successfully! Your download has started.'
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
console.error('Error during PDF decryption:', error);
|
console.error('Error during PDF decryption:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
if (error.name === 'PasswordException') {
|
|
||||||
showAlert('Incorrect Password', 'The password you entered is incorrect.');
|
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 {
|
} else {
|
||||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+96
-69
@@ -1,86 +1,113 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
initializeQpdf,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
|
||||||
import blobStream from 'blob-stream';
|
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
|
||||||
|
|
||||||
export async function encrypt() {
|
export async function encrypt() {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
const password = (
|
const userPassword =
|
||||||
document.getElementById('password-input') as HTMLInputElement
|
(document.getElementById('user-password-input') as HTMLInputElement)
|
||||||
).value;
|
?.value || '';
|
||||||
if (!password.trim()) {
|
const ownerPasswordInput =
|
||||||
showAlert('Input Required', 'Please enter a password.');
|
(document.getElementById('owner-password-input') as HTMLInputElement)
|
||||||
|
?.value || '';
|
||||||
|
|
||||||
|
if (!userPassword) {
|
||||||
|
showAlert('Input Required', 'Please enter a user password.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ownerPassword = ownerPasswordInput || userPassword;
|
||||||
|
const hasDistinctOwnerPassword = ownerPasswordInput !== '';
|
||||||
|
|
||||||
|
const inputPath = '/input.pdf';
|
||||||
|
const outputPath = '/output.pdf';
|
||||||
|
let qpdf: any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Preparing to process...');
|
showLoader('Initializing encryption...');
|
||||||
const pdfData = await readFileAsArrayBuffer(file);
|
qpdf = await initializeQpdf();
|
||||||
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer })
|
|
||||||
.promise;
|
|
||||||
const numPages = pdf.numPages;
|
|
||||||
const pageImages = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= numPages; i++) {
|
showLoader('Reading PDF...');
|
||||||
document.getElementById('loader-text').textContent =
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
`Processing page ${i} of ${numPages}...`;
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
const page = await pdf.getPage(i);
|
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const context = canvas.getContext('2d');
|
showLoader('Encrypting PDF with 256-bit AES...');
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
|
||||||
await page.render({
|
|
||||||
canvasContext: context,
|
// Only add restrictions if a distinct owner password was provided
|
||||||
viewport: viewport,
|
if (hasDistinctOwnerPassword) {
|
||||||
canvas: canvas,
|
args.push(
|
||||||
}).promise;
|
'--modify=none',
|
||||||
pageImages.push({
|
'--extract=n',
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
'--print=none',
|
||||||
width: viewport.width,
|
'--accessibility=n',
|
||||||
height: viewport.height,
|
'--annotate=n',
|
||||||
});
|
'--assemble=n',
|
||||||
|
'--form=n',
|
||||||
|
'--modify-other=n'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent =
|
args.push('--', outputPath);
|
||||||
'Encrypting and building PDF...';
|
|
||||||
const doc = new PDFDocument({
|
|
||||||
size: [pageImages[0].width, pageImages[0].height],
|
|
||||||
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
|
|
||||||
userPassword: password,
|
|
||||||
ownerPassword: password,
|
|
||||||
permissions: {
|
|
||||||
printing: 'highResolution',
|
|
||||||
modifying: false,
|
|
||||||
copying: false,
|
|
||||||
annotating: false,
|
|
||||||
fillingForms: false,
|
|
||||||
contentAccessibility: true,
|
|
||||||
documentAssembly: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const stream = doc.pipe(blobStream());
|
|
||||||
for (let i = 0; i < pageImages.length; i++) {
|
|
||||||
if (i > 0)
|
|
||||||
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
|
||||||
doc.image(pageImages[i].data, 0, 0, {
|
|
||||||
width: pageImages[i].width,
|
|
||||||
height: pageImages[i].height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
doc.end();
|
|
||||||
|
|
||||||
stream.on('finish', function () {
|
try {
|
||||||
const blob = stream.toBlob('application/pdf');
|
qpdf.callMain(args);
|
||||||
downloadFile(blob, `encrypted-${file.name}`);
|
} catch (qpdfError: any) {
|
||||||
hideLoader();
|
console.error('qpdf execution error:', qpdfError);
|
||||||
showAlert('Success', 'Encryption complete! Your download has started.');
|
throw new Error(
|
||||||
});
|
'Encryption failed: ' + (qpdfError.message || 'Unknown error')
|
||||||
} catch (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);
|
console.error('Error during PDF encryption:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,11 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
|
export function flattenFormsInDoc(pdfDoc) {
|
||||||
|
const form = pdfDoc.getForm();
|
||||||
|
form.flatten();
|
||||||
|
}
|
||||||
|
|
||||||
export async function flatten() {
|
export async function flatten() {
|
||||||
if (!state.pdfDoc) {
|
if (!state.pdfDoc) {
|
||||||
showAlert('Error', 'PDF not loaded.');
|
showAlert('Error', 'PDF not loaded.');
|
||||||
@@ -9,8 +14,7 @@ export async function flatten() {
|
|||||||
}
|
}
|
||||||
showLoader('Flattening PDF...');
|
showLoader('Flattening PDF...');
|
||||||
try {
|
try {
|
||||||
const form = state.pdfDoc.getForm();
|
flattenFormsInDoc(state.pdfDoc);
|
||||||
form.flatten();
|
|
||||||
|
|
||||||
const flattenedBytes = await state.pdfDoc.save();
|
const flattenedBytes = await state.pdfDoc.save();
|
||||||
downloadFile(
|
downloadFile(
|
||||||
|
|||||||
@@ -63,12 +63,15 @@ import {
|
|||||||
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
||||||
import { linearizePdf } from './linearize.js';
|
import { linearizePdf } from './linearize.js';
|
||||||
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
|
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
|
||||||
|
import { sanitizePdf } from './sanitize-pdf.js';
|
||||||
|
import { removeRestrictions } from './remove-restrictions.js';
|
||||||
|
|
||||||
export const toolLogic = {
|
export const toolLogic = {
|
||||||
merge: { process: merge, setup: setupMergeTool },
|
merge: { process: merge, setup: setupMergeTool },
|
||||||
split: { process: split, setup: setupSplitTool },
|
split: { process: split, setup: setupSplitTool },
|
||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
|
'remove-restrictions': removeRestrictions,
|
||||||
organize,
|
organize,
|
||||||
rotate,
|
rotate,
|
||||||
'add-page-numbers': addPageNumbers,
|
'add-page-numbers': addPageNumbers,
|
||||||
@@ -135,4 +138,5 @@ export const toolLogic = {
|
|||||||
process: addAttachments,
|
process: addAttachments,
|
||||||
setup: setupAddAttachmentsTool,
|
setup: setupAddAttachmentsTool,
|
||||||
},
|
},
|
||||||
|
'sanitize-pdf': sanitizePdf,
|
||||||
};
|
};
|
||||||
+54
-15
@@ -113,11 +113,9 @@ async function runOCR() {
|
|||||||
tessjs_create_hocr: '1',
|
tessjs_create_hocr: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (whitelist.trim()) {
|
await worker.setParameters({
|
||||||
await worker.setParameters({
|
tessedit_char_whitelist: whitelist,
|
||||||
tessedit_char_whitelist: whitelist.trim(),
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdf = await pdfjsLib.getDocument(
|
const pdf = await pdfjsLib.getDocument(
|
||||||
@@ -144,7 +142,11 @@ async function runOCR() {
|
|||||||
binarizeCanvas(context);
|
binarizeCanvas(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await worker.recognize(canvas, {}, { text: true, hocr: true });
|
const result = await worker.recognize(
|
||||||
|
canvas,
|
||||||
|
{},
|
||||||
|
{ text: true, hocr: true }
|
||||||
|
);
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
const pngImageBytes = await new Promise((resolve) =>
|
const pngImageBytes = await new Promise((resolve) =>
|
||||||
@@ -281,9 +283,51 @@ export function setupOcrTool() {
|
|||||||
);
|
);
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
|
// Whitelist presets
|
||||||
|
const whitelistPresets: Record<string, string> = {
|
||||||
|
alphanumeric:
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"',
|
||||||
|
'numbers-currency': '0123456789$€£¥.,- ',
|
||||||
|
'letters-only': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ',
|
||||||
|
'numbers-only': '0123456789',
|
||||||
|
invoice: '0123456789$.,/-#: ',
|
||||||
|
forms:
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,()-_/@#:',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle whitelist preset selection
|
||||||
|
const presetSelect = document.getElementById(
|
||||||
|
'whitelist-preset'
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
const whitelistInput = document.getElementById(
|
||||||
|
'ocr-whitelist'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
|
presetSelect?.addEventListener('change', (e) => {
|
||||||
|
const preset = (e.target as HTMLSelectElement).value;
|
||||||
|
if (preset && preset !== 'custom') {
|
||||||
|
whitelistInput.value = whitelistPresets[preset];
|
||||||
|
whitelistInput.disabled = true;
|
||||||
|
} else {
|
||||||
|
whitelistInput.disabled = false;
|
||||||
|
if (preset === '') {
|
||||||
|
whitelistInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle details toggle icon rotation
|
||||||
|
document.querySelectorAll('details').forEach((details) => {
|
||||||
|
details.addEventListener('toggle', () => {
|
||||||
|
const icon = details.querySelector('.details-icon') as HTMLElement;
|
||||||
|
if (icon) {
|
||||||
|
icon.style.transform = details.open ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
langSearch.addEventListener('input', () => {
|
langSearch.addEventListener('input', () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const searchTerm = (langSearch as HTMLInputElement).value.toLowerCase();
|
||||||
const searchTerm = langSearch.value.toLowerCase();
|
|
||||||
langList.querySelectorAll('label').forEach((label) => {
|
langList.querySelectorAll('label').forEach((label) => {
|
||||||
label.style.display = label.textContent.toLowerCase().includes(searchTerm)
|
label.style.display = label.textContent.toLowerCase().includes(searchTerm)
|
||||||
? ''
|
? ''
|
||||||
@@ -291,19 +335,14 @@ export function setupOcrTool() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the display of selected languages
|
|
||||||
langList.addEventListener('change', () => {
|
langList.addEventListener('change', () => {
|
||||||
const selected = Array.from(
|
const selected = Array.from(
|
||||||
langList.querySelectorAll('.lang-checkbox:checked')
|
langList.querySelectorAll('.lang-checkbox:checked')
|
||||||
)
|
).map((cb) => tesseractLanguages[(cb as HTMLInputElement).value]);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
|
||||||
.map((cb) => tesseractLanguages[cb.value]);
|
|
||||||
selectedLangsDisplay.textContent =
|
selectedLangsDisplay.textContent =
|
||||||
selected.length > 0 ? selected.join(', ') : 'None';
|
selected.length > 0 ? selected.join(', ') : 'None';
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
(processBtn as HTMLButtonElement).disabled = selected.length === 0;
|
||||||
processBtn.disabled = selected.length === 0;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attach the main OCR function to the process button
|
|
||||||
processBtn.addEventListener('click', runOCR);
|
processBtn.addEventListener('click', runOCR);
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,48 @@ export function setupRemoveAnnotationsTool() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeAnnotationsFromDoc(
|
||||||
|
pdfDoc,
|
||||||
|
pageIndices = null,
|
||||||
|
annotationTypes = null
|
||||||
|
) {
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
const targetPages =
|
||||||
|
pageIndices || Array.from({ length: pages.length }, (_, i) => i);
|
||||||
|
|
||||||
|
for (const pageIndex of targetPages) {
|
||||||
|
const page = pages[pageIndex];
|
||||||
|
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||||
|
|
||||||
|
if (!annotationTypes) {
|
||||||
|
if (annotRefs.length > 0) {
|
||||||
|
page.node.delete(PDFName.of('Annots'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const annotsToKeep = [];
|
||||||
|
|
||||||
|
for (const ref of annotRefs) {
|
||||||
|
const annot = pdfDoc.context.lookup(ref);
|
||||||
|
const subtype = annot
|
||||||
|
.get(PDFName.of('Subtype'))
|
||||||
|
?.toString()
|
||||||
|
.substring(1);
|
||||||
|
|
||||||
|
if (!subtype || !annotationTypes.has(subtype)) {
|
||||||
|
annotsToKeep.push(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (annotsToKeep.length > 0) {
|
||||||
|
const newAnnotsArray = pdfDoc.context.obj(annotsToKeep);
|
||||||
|
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||||
|
} else {
|
||||||
|
page.node.delete(PDFName.of('Annots'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function removeAnnotations() {
|
export async function removeAnnotations() {
|
||||||
showLoader('Removing annotations...');
|
showLoader('Removing annotations...');
|
||||||
try {
|
try {
|
||||||
@@ -80,34 +122,7 @@ export async function removeAnnotations() {
|
|||||||
if (typesToRemove.size === 0)
|
if (typesToRemove.size === 0)
|
||||||
throw new Error('Please select at least one annotation type to remove.');
|
throw new Error('Please select at least one annotation type to remove.');
|
||||||
|
|
||||||
const pages = state.pdfDoc.getPages();
|
removeAnnotationsFromDoc(state.pdfDoc, targetPageIndices, typesToRemove);
|
||||||
|
|
||||||
for (const pageIndex of targetPageIndices) {
|
|
||||||
const page = pages[pageIndex];
|
|
||||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
|
||||||
|
|
||||||
const annotsToKeep = [];
|
|
||||||
|
|
||||||
for (const ref of annotRefs) {
|
|
||||||
const annot = state.pdfDoc.context.lookup(ref);
|
|
||||||
const subtype = annot
|
|
||||||
.get(PDFName.of('Subtype'))
|
|
||||||
?.toString()
|
|
||||||
.substring(1);
|
|
||||||
|
|
||||||
// If the subtype is NOT in the list to remove, add it to our new array
|
|
||||||
if (!subtype || !typesToRemove.has(subtype)) {
|
|
||||||
annotsToKeep.push(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (annotsToKeep.length > 0) {
|
|
||||||
const newAnnotsArray = state.pdfDoc.context.obj(annotsToKeep);
|
|
||||||
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
|
||||||
} else {
|
|
||||||
page.node.delete(PDFName.of('Annots'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(
|
downloadFile(
|
||||||
|
|||||||
@@ -1,23 +1,54 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
import { PDFName } from 'pdf-lib';
|
||||||
|
|
||||||
|
export function removeMetadataFromDoc(pdfDoc) {
|
||||||
|
const infoDict = pdfDoc.getInfoDict();
|
||||||
|
const allKeys = infoDict.keys();
|
||||||
|
allKeys.forEach((key: any) => {
|
||||||
|
infoDict.delete(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
pdfDoc.setTitle('');
|
||||||
|
pdfDoc.setAuthor('');
|
||||||
|
pdfDoc.setSubject('');
|
||||||
|
pdfDoc.setKeywords([]);
|
||||||
|
pdfDoc.setCreator('');
|
||||||
|
pdfDoc.setProducer('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
if (catalogDict.has(PDFName.of('Metadata'))) {
|
||||||
|
catalogDict.delete(PDFName.of('Metadata'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove XMP metadata:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = pdfDoc.context;
|
||||||
|
if (context.trailerInfo) {
|
||||||
|
delete context.trailerInfo.ID;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove document IDs:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
if (catalogDict.has(PDFName.of('PieceInfo'))) {
|
||||||
|
catalogDict.delete(PDFName.of('PieceInfo'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove PieceInfo:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function removeMetadata() {
|
export async function removeMetadata() {
|
||||||
showLoader('Removing all metadata...');
|
showLoader('Removing all metadata...');
|
||||||
try {
|
try {
|
||||||
const infoDict = state.pdfDoc.getInfoDict();
|
removeMetadataFromDoc(state.pdfDoc);
|
||||||
|
|
||||||
const allKeys = infoDict.keys();
|
|
||||||
allKeys.forEach((key: any) => {
|
|
||||||
infoDict.delete(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
state.pdfDoc.setTitle('');
|
|
||||||
state.pdfDoc.setAuthor('');
|
|
||||||
state.pdfDoc.setSubject('');
|
|
||||||
state.pdfDoc.setKeywords([]);
|
|
||||||
state.pdfDoc.setCreator('');
|
|
||||||
state.pdfDoc.setProducer('');
|
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(
|
downloadFile(
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
|
import {
|
||||||
|
downloadFile,
|
||||||
|
initializeQpdf,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
|
||||||
|
export async function removeRestrictions() {
|
||||||
|
const file = state.files[0];
|
||||||
|
const password =
|
||||||
|
(document.getElementById('owner-password-remove') 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('Removing restrictions...');
|
||||||
|
|
||||||
|
const args = [inputPath];
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
args.push(`--password=${password}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push('--decrypt', '--remove-restrictions', '--', outputPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
qpdf.callMain(args);
|
||||||
|
} catch (qpdfError: any) {
|
||||||
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
if (
|
||||||
|
qpdfError.message?.includes('password') ||
|
||||||
|
qpdfError.message?.includes('encrypt')
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Failed to remove restrictions. The PDF may require the correct owner password.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'Failed to remove restrictions: ' +
|
||||||
|
(qpdfError.message || 'Unknown error')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Preparing download...');
|
||||||
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
|
||||||
|
if (!outputFile || outputFile.length === 0) {
|
||||||
|
throw new Error('Operation resulted in an empty file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
|
downloadFile(blob, `unrestricted-${file.name}`);
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'PDF restrictions removed successfully! The file is now fully editable and printable.'
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error during restriction removal:', error);
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Operation 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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
|||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
export async function reversePages() {
|
export async function reversePages() {
|
||||||
const pdfDocs = state.files.filter((file: File) => file.type === 'application/pdf');
|
const pdfDocs = state.files.filter(
|
||||||
|
(file: File) => file.type === 'application/pdf'
|
||||||
|
);
|
||||||
if (!pdfDocs.length) {
|
if (!pdfDocs.length) {
|
||||||
showAlert('Error', 'PDF not loaded.');
|
showAlert('Error', 'PDF not loaded.');
|
||||||
return;
|
return;
|
||||||
@@ -16,7 +18,7 @@ export async function reversePages() {
|
|||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
for (let j = 0; j < pdfDocs.length; j++) {
|
for (let j = 0; j < pdfDocs.length; j++) {
|
||||||
const file = pdfDocs[j];
|
const file = pdfDocs[j];
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const pageCount = pdfDoc.getPageCount();
|
const pageCount = pdfDoc.getPageCount();
|
||||||
|
|||||||
@@ -0,0 +1,587 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { removeMetadataFromDoc } from './remove-metadata.js';
|
||||||
|
import { removeAnnotationsFromDoc } from './remove-annotations.js';
|
||||||
|
import { flattenFormsInDoc } from './flatten.js';
|
||||||
|
import { PDFName } from 'pdf-lib';
|
||||||
|
|
||||||
|
export async function sanitizePdf() {
|
||||||
|
if (!state.pdfDoc) {
|
||||||
|
showAlert('Error', 'No PDF document loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Sanitizing PDF...');
|
||||||
|
try {
|
||||||
|
const pdfDoc = state.pdfDoc;
|
||||||
|
|
||||||
|
const shouldFlattenForms = (
|
||||||
|
document.getElementById('flatten-forms') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const shouldRemoveMetadata = (
|
||||||
|
document.getElementById('remove-metadata') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const shouldRemoveAnnotations = (
|
||||||
|
document.getElementById('remove-annotations') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const shouldRemoveJavascript = (
|
||||||
|
document.getElementById('remove-javascript') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const shouldRemoveEmbeddedFiles = (
|
||||||
|
document.getElementById('remove-embedded-files') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const shouldRemoveLayers = (
|
||||||
|
document.getElementById('remove-layers') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const shouldRemoveLinks = (
|
||||||
|
document.getElementById('remove-links') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const shouldRemoveStructureTree = (
|
||||||
|
document.getElementById('remove-structure-tree') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const shouldRemoveMarkInfo = (
|
||||||
|
document.getElementById('remove-markinfo') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const shouldRemoveFonts = (
|
||||||
|
document.getElementById('remove-fonts') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
|
||||||
|
let changesMade = false;
|
||||||
|
|
||||||
|
if (shouldFlattenForms) {
|
||||||
|
try {
|
||||||
|
flattenFormsInDoc(pdfDoc);
|
||||||
|
changesMade = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not flatten forms: ${e.message}`);
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
if (catalogDict.has(PDFName.of('AcroForm'))) {
|
||||||
|
catalogDict.delete(PDFName.of('AcroForm'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (removeError) {
|
||||||
|
console.warn('Could not remove AcroForm:', removeError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveMetadata) {
|
||||||
|
removeMetadataFromDoc(pdfDoc);
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveAnnotations) {
|
||||||
|
removeAnnotationsFromDoc(pdfDoc);
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveJavascript) {
|
||||||
|
try {
|
||||||
|
if (pdfDoc.javaScripts && pdfDoc.javaScripts.length > 0) {
|
||||||
|
pdfDoc.javaScripts = [];
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
|
||||||
|
const namesRef = catalogDict.get(PDFName.of('Names'));
|
||||||
|
if (namesRef) {
|
||||||
|
try {
|
||||||
|
const namesDict = pdfDoc.context.lookup(namesRef);
|
||||||
|
if (namesDict.has(PDFName.of('JavaScript'))) {
|
||||||
|
namesDict.delete(PDFName.of('JavaScript'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not access Names/JavaScript:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('OpenAction'))) {
|
||||||
|
catalogDict.delete(PDFName.of('OpenAction'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('AA'))) {
|
||||||
|
catalogDict.delete(PDFName.of('AA'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
for (const page of pages) {
|
||||||
|
try {
|
||||||
|
const pageDict = page.node;
|
||||||
|
|
||||||
|
if (pageDict.has(PDFName.of('AA'))) {
|
||||||
|
pageDict.delete(PDFName.of('AA'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const annotRefs = pageDict.Annots()?.asArray() || [];
|
||||||
|
for (const annotRef of annotRefs) {
|
||||||
|
try {
|
||||||
|
const annot = pdfDoc.context.lookup(annotRef);
|
||||||
|
|
||||||
|
if (annot.has(PDFName.of('A'))) {
|
||||||
|
const actionRef = annot.get(PDFName.of('A'));
|
||||||
|
try {
|
||||||
|
const actionDict = pdfDoc.context.lookup(actionRef);
|
||||||
|
const actionType = actionDict
|
||||||
|
.get(PDFName.of('S'))
|
||||||
|
?.toString()
|
||||||
|
.substring(1);
|
||||||
|
|
||||||
|
if (actionType === 'JavaScript') {
|
||||||
|
annot.delete(PDFName.of('A'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not read action:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (annot.has(PDFName.of('AA'))) {
|
||||||
|
annot.delete(PDFName.of('AA'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not process annotation for JS:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove page actions:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const acroFormRef = catalogDict.get(PDFName.of('AcroForm'));
|
||||||
|
if (acroFormRef) {
|
||||||
|
const acroFormDict = pdfDoc.context.lookup(acroFormRef);
|
||||||
|
const fieldsRef = acroFormDict.get(PDFName.of('Fields'));
|
||||||
|
|
||||||
|
if (fieldsRef) {
|
||||||
|
const fieldsArray = pdfDoc.context.lookup(fieldsRef);
|
||||||
|
const fields = fieldsArray.asArray();
|
||||||
|
|
||||||
|
for (const fieldRef of fields) {
|
||||||
|
try {
|
||||||
|
const field = pdfDoc.context.lookup(fieldRef);
|
||||||
|
|
||||||
|
if (field.has(PDFName.of('A'))) {
|
||||||
|
field.delete(PDFName.of('A'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.has(PDFName.of('AA'))) {
|
||||||
|
field.delete(PDFName.of('AA'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not process field for JS:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not process form fields for JS:', e.message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not remove JavaScript: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveEmbeddedFiles) {
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
|
||||||
|
const namesRef = catalogDict.get(PDFName.of('Names'));
|
||||||
|
if (namesRef) {
|
||||||
|
try {
|
||||||
|
const namesDict = pdfDoc.context.lookup(namesRef);
|
||||||
|
if (namesDict.has(PDFName.of('EmbeddedFiles'))) {
|
||||||
|
namesDict.delete(PDFName.of('EmbeddedFiles'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not access Names/EmbeddedFiles:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('EmbeddedFiles'))) {
|
||||||
|
catalogDict.delete(PDFName.of('EmbeddedFiles'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
for (const page of pages) {
|
||||||
|
try {
|
||||||
|
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||||
|
const annotsToKeep = [];
|
||||||
|
|
||||||
|
for (const ref of annotRefs) {
|
||||||
|
try {
|
||||||
|
const annot = pdfDoc.context.lookup(ref);
|
||||||
|
const subtype = annot
|
||||||
|
.get(PDFName.of('Subtype'))
|
||||||
|
?.toString()
|
||||||
|
.substring(1);
|
||||||
|
|
||||||
|
if (subtype !== 'FileAttachment') {
|
||||||
|
annotsToKeep.push(ref);
|
||||||
|
} else {
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
annotsToKeep.push(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (annotsToKeep.length !== annotRefs.length) {
|
||||||
|
if (annotsToKeep.length > 0) {
|
||||||
|
const newAnnotsArray = pdfDoc.context.obj(annotsToKeep);
|
||||||
|
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||||
|
} else {
|
||||||
|
page.node.delete(PDFName.of('Annots'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (pageError) {
|
||||||
|
console.warn(
|
||||||
|
`Could not process page for attachments: ${pageError.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdfDoc.embeddedFiles && pdfDoc.embeddedFiles.length > 0) {
|
||||||
|
pdfDoc.embeddedFiles = [];
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('Collection'))) {
|
||||||
|
catalogDict.delete(PDFName.of('Collection'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not remove embedded files: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveLayers) {
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('OCProperties'))) {
|
||||||
|
catalogDict.delete(PDFName.of('OCProperties'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
for (const page of pages) {
|
||||||
|
try {
|
||||||
|
const pageDict = page.node;
|
||||||
|
|
||||||
|
if (pageDict.has(PDFName.of('OCProperties'))) {
|
||||||
|
pageDict.delete(PDFName.of('OCProperties'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourcesRef = pageDict.get(PDFName.of('Resources'));
|
||||||
|
if (resourcesRef) {
|
||||||
|
try {
|
||||||
|
const resourcesDict = pdfDoc.context.lookup(resourcesRef);
|
||||||
|
if (resourcesDict.has(PDFName.of('Properties'))) {
|
||||||
|
resourcesDict.delete(PDFName.of('Properties'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not access Resources:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove page layers:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not remove layers: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:@ALAM
|
||||||
|
// Currently if the links are embedded in a stream they can't be removed
|
||||||
|
// Find a way to remove them from the stream
|
||||||
|
if (shouldRemoveLinks) {
|
||||||
|
try {
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
|
for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
|
||||||
|
try {
|
||||||
|
const page = pages[pageIndex];
|
||||||
|
const pageDict = page.node;
|
||||||
|
|
||||||
|
const annotsRef = pageDict.get(PDFName.of('Annots'));
|
||||||
|
if (!annotsRef) continue;
|
||||||
|
|
||||||
|
const annotsArray = pdfDoc.context.lookup(annotsRef);
|
||||||
|
const annotRefs = annotsArray.asArray();
|
||||||
|
|
||||||
|
if (annotRefs.length === 0) continue;
|
||||||
|
|
||||||
|
const annotsToKeep = [];
|
||||||
|
let linksRemoved = 0;
|
||||||
|
|
||||||
|
for (const ref of annotRefs) {
|
||||||
|
try {
|
||||||
|
const annot = pdfDoc.context.lookup(ref);
|
||||||
|
const subtype = annot
|
||||||
|
.get(PDFName.of('Subtype'))
|
||||||
|
?.toString()
|
||||||
|
.substring(1);
|
||||||
|
|
||||||
|
let isLink = false;
|
||||||
|
|
||||||
|
if (subtype === 'Link') {
|
||||||
|
isLink = true;
|
||||||
|
linksRemoved++;
|
||||||
|
} else {
|
||||||
|
const actionRef = annot.get(PDFName.of('A'));
|
||||||
|
if (actionRef) {
|
||||||
|
try {
|
||||||
|
const actionDict = pdfDoc.context.lookup(actionRef);
|
||||||
|
const actionType = actionDict
|
||||||
|
.get(PDFName.of('S'))
|
||||||
|
?.toString()
|
||||||
|
.substring(1);
|
||||||
|
|
||||||
|
if (
|
||||||
|
actionType === 'URI' ||
|
||||||
|
actionType === 'Launch' ||
|
||||||
|
actionType === 'GoTo' ||
|
||||||
|
actionType === 'GoToR'
|
||||||
|
) {
|
||||||
|
isLink = true;
|
||||||
|
linksRemoved++;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not read action:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dest = annot.get(PDFName.of('Dest'));
|
||||||
|
if (dest && !isLink) {
|
||||||
|
isLink = true;
|
||||||
|
linksRemoved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLink) {
|
||||||
|
annotsToKeep.push(ref);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not process annotation:', e.message);
|
||||||
|
annotsToKeep.push(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linksRemoved > 0) {
|
||||||
|
if (annotsToKeep.length > 0) {
|
||||||
|
const newAnnotsArray = pdfDoc.context.obj(annotsToKeep);
|
||||||
|
pageDict.set(PDFName.of('Annots'), newAnnotsArray);
|
||||||
|
} else {
|
||||||
|
pageDict.delete(PDFName.of('Annots'));
|
||||||
|
}
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (pageError) {
|
||||||
|
console.warn(
|
||||||
|
`Could not process page ${pageIndex + 1} for links: ${pageError.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
const namesRef = catalogDict.get(PDFName.of('Names'));
|
||||||
|
if (namesRef) {
|
||||||
|
try {
|
||||||
|
const namesDict = pdfDoc.context.lookup(namesRef);
|
||||||
|
if (namesDict.has(PDFName.of('Dests'))) {
|
||||||
|
namesDict.delete(PDFName.of('Dests'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not access Names/Dests:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('Dests'))) {
|
||||||
|
catalogDict.delete(PDFName.of('Dests'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove named destinations:', e.message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not remove links: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveStructureTree) {
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('StructTreeRoot'))) {
|
||||||
|
catalogDict.delete(PDFName.of('StructTreeRoot'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
for (const page of pages) {
|
||||||
|
try {
|
||||||
|
const pageDict = page.node;
|
||||||
|
if (pageDict.has(PDFName.of('StructParents'))) {
|
||||||
|
pageDict.delete(PDFName.of('StructParents'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove page StructParents:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('ParentTree'))) {
|
||||||
|
catalogDict.delete(PDFName.of('ParentTree'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not remove structure tree: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveMarkInfo) {
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('MarkInfo'))) {
|
||||||
|
catalogDict.delete(PDFName.of('MarkInfo'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catalogDict.has(PDFName.of('Marked'))) {
|
||||||
|
catalogDict.delete(PDFName.of('Marked'));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not remove MarkInfo: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveFonts) {
|
||||||
|
try {
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
|
for (let pageIndex = 0; pageIndex < pages.length; pageIndex++) {
|
||||||
|
try {
|
||||||
|
const page = pages[pageIndex];
|
||||||
|
const pageDict = page.node;
|
||||||
|
const resourcesRef = pageDict.get(PDFName.of('Resources'));
|
||||||
|
|
||||||
|
if (resourcesRef) {
|
||||||
|
try {
|
||||||
|
const resourcesDict = pdfDoc.context.lookup(resourcesRef);
|
||||||
|
|
||||||
|
if (resourcesDict.has(PDFName.of('Font'))) {
|
||||||
|
const fontRef = resourcesDict.get(PDFName.of('Font'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fontDict = pdfDoc.context.lookup(fontRef);
|
||||||
|
const fontKeys = fontDict.keys();
|
||||||
|
|
||||||
|
for (const fontKey of fontKeys) {
|
||||||
|
try {
|
||||||
|
const specificFontRef = fontDict.get(fontKey);
|
||||||
|
const specificFont =
|
||||||
|
pdfDoc.context.lookup(specificFontRef);
|
||||||
|
|
||||||
|
if (specificFont.has(PDFName.of('FontDescriptor'))) {
|
||||||
|
const descriptorRef = specificFont.get(
|
||||||
|
PDFName.of('FontDescriptor')
|
||||||
|
);
|
||||||
|
const descriptor =
|
||||||
|
pdfDoc.context.lookup(descriptorRef);
|
||||||
|
|
||||||
|
const fontFileKeys = [
|
||||||
|
'FontFile',
|
||||||
|
'FontFile2',
|
||||||
|
'FontFile3',
|
||||||
|
];
|
||||||
|
for (const key of fontFileKeys) {
|
||||||
|
if (descriptor.has(PDFName.of(key))) {
|
||||||
|
descriptor.delete(PDFName.of(key));
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users/Developers: Uncomment this if you can delete the entire font entry -- might break the rendering though
|
||||||
|
// fontDict.delete(fontKey);
|
||||||
|
// changesMade = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
`Could not process font ${fontKey}:`,
|
||||||
|
e.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
'Could not access font dictionary:',
|
||||||
|
e.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
'Could not access Resources for fonts:',
|
||||||
|
e.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
`Could not remove fonts from page ${pageIndex + 1}:`,
|
||||||
|
e.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdfDoc.fonts && pdfDoc.fonts.length > 0) {
|
||||||
|
pdfDoc.fonts = [];
|
||||||
|
changesMade = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Could not remove fonts: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changesMade) {
|
||||||
|
showAlert(
|
||||||
|
'No Changes',
|
||||||
|
'No items were selected for removal or none were found in the PDF.'
|
||||||
|
);
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedPdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([sanitizedPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'sanitized.pdf'
|
||||||
|
);
|
||||||
|
showAlert('Success', 'PDF has been sanitized and downloaded.');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Sanitization Error:', e);
|
||||||
|
showAlert('Error', `An error occurred during sanitization: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
-69
@@ -5,82 +5,120 @@ import { createIcons, icons } from 'lucide';
|
|||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import '../css/styles.css';
|
import '../css/styles.css';
|
||||||
|
|
||||||
const hideBrandingSections = () => {
|
|
||||||
const nav = document.querySelector('nav');
|
|
||||||
if (nav) {
|
|
||||||
nav.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const heroSection = document.getElementById('hero-section');
|
|
||||||
if (heroSection) {
|
|
||||||
heroSection.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const featuresSection = document.getElementById('features-section');
|
|
||||||
if (featuresSection) {
|
|
||||||
featuresSection.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const securitySection = document.getElementById('security-compliance-section');
|
|
||||||
if (securitySection) {
|
|
||||||
securitySection.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const faqSection = document.getElementById('faq-accordion');
|
|
||||||
if (faqSection) {
|
|
||||||
faqSection.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const testimonialsSection = document.getElementById('testimonials-section');
|
|
||||||
if (testimonialsSection) {
|
|
||||||
testimonialsSection.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const supportSection = document.getElementById('support-section');
|
|
||||||
if (supportSection) {
|
|
||||||
supportSection.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const footer = document.querySelector('footer');
|
|
||||||
if (footer) {
|
|
||||||
footer.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
const sectionDividers = document.querySelectorAll('.section-divider');
|
|
||||||
sectionDividers.forEach(divider => {
|
|
||||||
(divider as HTMLElement).style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.title = 'PDF Tools';
|
|
||||||
|
|
||||||
const toolsHeader = document.getElementById('tools-header');
|
|
||||||
if (toolsHeader) {
|
|
||||||
const title = toolsHeader.querySelector('h2');
|
|
||||||
const subtitle = toolsHeader.querySelector('p');
|
|
||||||
if (title) {
|
|
||||||
title.textContent = 'PDF Tools';
|
|
||||||
title.className = 'text-4xl md:text-5xl font-bold text-white mb-3';
|
|
||||||
}
|
|
||||||
if (subtitle) {
|
|
||||||
subtitle.textContent = 'Select a tool to get started';
|
|
||||||
subtitle.className = 'text-lg text-gray-400';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = document.getElementById('app');
|
|
||||||
if (app) {
|
|
||||||
app.style.paddingTop = '2rem';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
// Handle simple mode - hide branding sections
|
// Handle simple mode - hide branding sections but keep logo and copyright
|
||||||
if (__SIMPLE_MODE__) {
|
if (__SIMPLE_MODE__) {
|
||||||
|
const hideBrandingSections = () => {
|
||||||
|
// Hide navigation but keep logo
|
||||||
|
const nav = document.querySelector('nav');
|
||||||
|
if (nav) {
|
||||||
|
// Hide the entire nav but we'll create a minimal one with just logo
|
||||||
|
nav.style.display = 'none';
|
||||||
|
|
||||||
|
// Create a simple nav with just logo on the right
|
||||||
|
const simpleNav = document.createElement('nav');
|
||||||
|
simpleNav.className =
|
||||||
|
'bg-gray-800 border-b border-gray-700 sticky top-0 z-30';
|
||||||
|
simpleNav.innerHTML = `
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="flex justify-start items-center h-16">
|
||||||
|
<div class="flex-shrink-0 flex items-center">
|
||||||
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
||||||
|
<span class="text-white font-bold text-xl ml-2">BentoPDF</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.insertBefore(simpleNav, document.body.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const heroSection = document.getElementById('hero-section');
|
||||||
|
if (heroSection) {
|
||||||
|
heroSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const featuresSection = document.getElementById('features-section');
|
||||||
|
if (featuresSection) {
|
||||||
|
featuresSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const securitySection = document.getElementById(
|
||||||
|
'security-compliance-section'
|
||||||
|
);
|
||||||
|
if (securitySection) {
|
||||||
|
securitySection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const faqSection = document.getElementById('faq-accordion');
|
||||||
|
if (faqSection) {
|
||||||
|
faqSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const testimonialsSection = document.getElementById(
|
||||||
|
'testimonials-section'
|
||||||
|
);
|
||||||
|
if (testimonialsSection) {
|
||||||
|
testimonialsSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportSection = document.getElementById('support-section');
|
||||||
|
if (supportSection) {
|
||||||
|
supportSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide footer but keep copyright
|
||||||
|
const footer = document.querySelector('footer');
|
||||||
|
if (footer) {
|
||||||
|
footer.style.display = 'none';
|
||||||
|
|
||||||
|
const simpleFooter = document.createElement('footer');
|
||||||
|
simpleFooter.className = 'mt-16 border-t-2 border-gray-700 py-8';
|
||||||
|
simpleFooter.innerHTML = `
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
|
||||||
|
<span class="text-white font-bold text-lg">BentoPDF</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(simpleFooter);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionDividers = document.querySelectorAll('.section-divider');
|
||||||
|
sectionDividers.forEach((divider) => {
|
||||||
|
(divider as HTMLElement).style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.title = 'BentoPDF - PDF Tools';
|
||||||
|
|
||||||
|
const toolsHeader = document.getElementById('tools-header');
|
||||||
|
if (toolsHeader) {
|
||||||
|
const title = toolsHeader.querySelector('h2');
|
||||||
|
const subtitle = toolsHeader.querySelector('p');
|
||||||
|
if (title) {
|
||||||
|
title.textContent = 'PDF Tools';
|
||||||
|
title.className = 'text-4xl md:text-5xl font-bold text-white mb-3';
|
||||||
|
}
|
||||||
|
if (subtitle) {
|
||||||
|
subtitle.textContent = 'Select a tool to get started';
|
||||||
|
subtitle.className = 'text-lg text-gray-400';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = document.getElementById('app');
|
||||||
|
if (app) {
|
||||||
|
app.style.paddingTop = '1rem';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
hideBrandingSections();
|
hideBrandingSections();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+49
-44
@@ -1,48 +1,53 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
const mobileMenu = document.getElementById('mobile-menu');
|
const mobileMenu = document.getElementById('mobile-menu');
|
||||||
const menuIcon = document.getElementById('menu-icon');
|
const menuIcon = document.getElementById('menu-icon');
|
||||||
const closeIcon = document.getElementById('close-icon');
|
const closeIcon = document.getElementById('close-icon');
|
||||||
|
|
||||||
if (mobileMenuButton && mobileMenu && menuIcon && closeIcon) {
|
if (mobileMenuButton && mobileMenu && menuIcon && closeIcon) {
|
||||||
// Toggle menu on button click
|
// Toggle menu on button click
|
||||||
mobileMenuButton.addEventListener('click', () => {
|
mobileMenuButton.addEventListener('click', () => {
|
||||||
const isExpanded = mobileMenuButton.getAttribute('aria-expanded') === 'true';
|
const isExpanded =
|
||||||
|
mobileMenuButton.getAttribute('aria-expanded') === 'true';
|
||||||
// Toggle menu visibility
|
|
||||||
mobileMenu.classList.toggle('hidden');
|
|
||||||
|
|
||||||
// Toggle icons
|
|
||||||
menuIcon.classList.toggle('hidden');
|
|
||||||
closeIcon.classList.toggle('hidden');
|
|
||||||
|
|
||||||
// Update aria-expanded for accessibility
|
|
||||||
mobileMenuButton.setAttribute('aria-expanded', (!isExpanded).toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close menu when clicking on a link
|
// Toggle menu visibility
|
||||||
const mobileLinks = mobileMenu.querySelectorAll('a');
|
mobileMenu.classList.toggle('hidden');
|
||||||
mobileLinks.forEach(link => {
|
|
||||||
link.addEventListener('click', () => {
|
|
||||||
mobileMenu.classList.add('hidden');
|
|
||||||
menuIcon.classList.remove('hidden');
|
|
||||||
closeIcon.classList.add('hidden');
|
|
||||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close menu when clicking outside
|
// Toggle icons
|
||||||
document.addEventListener('click', (event) => {
|
menuIcon.classList.toggle('hidden');
|
||||||
const target = event.target as Node;
|
closeIcon.classList.toggle('hidden');
|
||||||
const isClickInsideMenu = mobileMenu.contains(target);
|
|
||||||
const isClickOnButton = mobileMenuButton.contains(target);
|
// Update aria-expanded for accessibility
|
||||||
|
mobileMenuButton.setAttribute('aria-expanded', (!isExpanded).toString());
|
||||||
if (!isClickInsideMenu && !isClickOnButton && !mobileMenu.classList.contains('hidden')) {
|
});
|
||||||
mobileMenu.classList.add('hidden');
|
|
||||||
menuIcon.classList.remove('hidden');
|
// Close menu when clicking on a link
|
||||||
closeIcon.classList.add('hidden');
|
const mobileLinks = mobileMenu.querySelectorAll('a');
|
||||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
mobileLinks.forEach((link) => {
|
||||||
}
|
link.addEventListener('click', () => {
|
||||||
});
|
mobileMenu.classList.add('hidden');
|
||||||
}
|
menuIcon.classList.remove('hidden');
|
||||||
});
|
closeIcon.classList.add('hidden');
|
||||||
|
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close menu when clicking outside
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const target = event.target as Node;
|
||||||
|
const isClickInsideMenu = mobileMenu.contains(target);
|
||||||
|
const isClickOnButton = mobileMenuButton.contains(target);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isClickInsideMenu &&
|
||||||
|
!isClickOnButton &&
|
||||||
|
!mobileMenu.classList.contains('hidden')
|
||||||
|
) {
|
||||||
|
mobileMenu.classList.add('hidden');
|
||||||
|
menuIcon.classList.remove('hidden');
|
||||||
|
closeIcon.classList.add('hidden');
|
||||||
|
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
+253
-40
@@ -379,23 +379,73 @@ export const toolTemplates = {
|
|||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
encrypt: () => `
|
encrypt: () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Encrypt PDF</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">Encrypt PDF</h2>
|
||||||
<p class="mb-6 text-gray-400">Upload a PDF to create a new, password-protected version.</p>
|
<p class="mb-6 text-gray-400">Add 256-bit AES password protection to your PDF.</p>
|
||||||
${createFileInputHTML()}
|
${createFileInputHTML()}
|
||||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
<div id="encrypt-options" class="hidden space-y-4 mt-6">
|
<div id="encrypt-options" class="hidden space-y-4 mt-6">
|
||||||
<div>
|
<div>
|
||||||
<label for="password-input" class="block mb-2 text-sm font-medium text-gray-300">Set Encryption Password</label>
|
<label for="user-password-input" class="block mb-2 text-sm font-medium text-gray-300">User Password</label>
|
||||||
<input type="password" id="password-input" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Enter a strong password">
|
<input required type="password" id="user-password-input" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password to open the PDF">
|
||||||
</div>
|
<p class="text-xs text-gray-500 mt-1">Required to open and view the PDF</p>
|
||||||
<div class="p-4 bg-gray-900 border border-yellow-500/30 text-yellow-200 rounded-lg">
|
</div>
|
||||||
<h3 class="font-semibold text-base mb-2">Important Note</h3>
|
<div>
|
||||||
<p class="text-sm text-gray-400">To create the secure file, each page is converted into an image. This means you won't be able to select text or click links in the final encrypted PDF.</p>
|
<label for="owner-password-input" class="block mb-2 text-sm font-medium text-gray-300">Owner Password (Optional)</label>
|
||||||
</div>
|
<input type="password" id="owner-password-input" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password for full permissions (recommended)">
|
||||||
<button id="process-btn" class="btn-gradient w-full mt-6">Encrypt & Download</button>
|
<p class="text-xs text-gray-500 mt-1">Allows changing permissions and removing encryption</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Restriction checkboxes (shown when owner password is entered) -->
|
||||||
|
<div id="restriction-options" class="hidden p-4 bg-gray-800 border border-gray-700 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-base mb-2 text-white">🔒 Restrict PDF Permissions</h3>
|
||||||
|
<p class="text-sm text-gray-400 mb-3">Select which actions to disable:</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="restrict-modify" checked>
|
||||||
|
<span>Disable all modifications (--modify=none)</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="restrict-extract" checked>
|
||||||
|
<span>Disable text and image extraction (--extract=n)</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="restrict-print" checked>
|
||||||
|
<span>Disable all printing (--print=none)</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="restrict-accessibility">
|
||||||
|
<span>Disable accessibility text copying (--accessibility=n)</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="restrict-annotate">
|
||||||
|
<span>Disable annotations (--annotate=n)</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="restrict-assemble">
|
||||||
|
<span>Disable page assembly (--assemble=n)</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="restrict-form">
|
||||||
|
<span>Disable form filling (--form=n)</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2">
|
||||||
|
<input type="checkbox" id="restrict-modify-other">
|
||||||
|
<span>Disable other modifications (--modify-other=n)</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="pdf-canvas" class="hidden"></canvas>
|
</div>
|
||||||
`,
|
|
||||||
|
<div class="p-4 bg-yellow-900/20 border border-yellow-500/30 text-yellow-200 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-base mb-2">⚠️ Security Recommendation</h3>
|
||||||
|
<p class="text-sm text-gray-300">For strong security, set both passwords. Without an owner password, the security restrictions (printing, copying, etc.) can be easily bypassed.</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 bg-green-900/20 border border-green-500/30 text-green-200 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-base mb-2">✓ High-Quality Encryption</h3>
|
||||||
|
<p class="text-sm text-gray-300">256-bit AES encryption without quality loss. Text remains selectable and searchable.</p>
|
||||||
|
</div>
|
||||||
|
<button id="process-btn" class="btn-gradient w-full mt-6">Encrypt & Download</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
decrypt: () => `
|
decrypt: () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Decrypt PDF</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">Decrypt PDF</h2>
|
||||||
<p class="mb-6 text-gray-400">Upload an encrypted PDF and provide its password to create an unlocked version.</p>
|
<p class="mb-6 text-gray-400">Upload an encrypted PDF and provide its password to create an unlocked version.</p>
|
||||||
@@ -865,50 +915,71 @@ export const toolTemplates = {
|
|||||||
|
|
||||||
'change-permissions': () => `
|
'change-permissions': () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Change PDF Permissions</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">Change PDF Permissions</h2>
|
||||||
<p class="mb-6 text-gray-400">Unlock a PDF and re-save it with new passwords and permissions.</p>
|
<p class="mb-6 text-gray-400">Modify passwords and permissions without losing quality.</p>
|
||||||
${createFileInputHTML()}
|
${createFileInputHTML()}
|
||||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
<div id="permissions-options" class="hidden mt-6 space-y-4">
|
<div id="permissions-options" class="hidden mt-6 space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="current-password" class="block mb-2 text-sm font-medium text-gray-300">Current Password (to unlock)</label>
|
<label for="current-password" class="block mb-2 text-sm font-medium text-gray-300">Current Password (if encrypted)</label>
|
||||||
<input type="password" id="current-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Enter current password to open file">
|
<input type="password" id="current-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Leave blank if PDF is not password-protected">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="new-user-password" class="block mb-2 text-sm font-medium text-gray-300">New User Password (optional)</label>
|
<label for="new-user-password" class="block mb-2 text-sm font-medium text-gray-300">New User Password (optional)</label>
|
||||||
<input type="password" id="new-user-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password to open file">
|
<input type="password" id="new-user-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password to open PDF">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="new-owner-password" class="block mb-2 text-sm font-medium text-gray-300">New Owner Password (optional)</label>
|
<label for="new-owner-password" class="block mb-2 text-sm font-medium text-gray-300">New Owner Password (optional)</label>
|
||||||
<input type="password" id="new-owner-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Admin password for permissions">
|
<input type="password" id="new-owner-password" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Password for full permissions">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 bg-gray-900 border border-blue-500/30 text-blue-200 rounded-lg">
|
<div class="p-4 bg-blue-900/20 border border-blue-500/30 text-blue-200 rounded-lg">
|
||||||
<h3 class="font-semibold text-base mb-2">How Passwords Work</h3>
|
<h3 class="font-semibold text-base mb-2">How It Works</h3>
|
||||||
<ul class="list-disc list-inside text-sm text-gray-400 space-y-1">
|
<ul class="list-disc list-inside text-sm text-gray-300 space-y-1">
|
||||||
<li>The <strong>User Password</strong> is required to open and decrypt the PDF.</li>
|
<li><strong>User Password:</strong> Required to open the PDF</li>
|
||||||
<li>The <strong>Owner Password</strong> is an admin key that bypasses all permissions.</li>
|
<li><strong>Owner Password:</strong> Required to enforce the permissions below</li>
|
||||||
<li>Leave both blank to create an unprotected PDF.</li>
|
<li>Leave both blank to remove all encryption and restrictions</li>
|
||||||
<li>Set an Owner Password to enforce the permissions you select below.</li>
|
<li>Check boxes below to ALLOW specific actions (unchecked = disabled)</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset class="border border-gray-600 p-4 rounded-lg">
|
<fieldset class="border border-gray-600 p-4 rounded-lg">
|
||||||
<legend class="px-2 text-sm font-medium text-gray-300">Allow User to:</legend>
|
<legend class="px-2 text-sm font-medium text-gray-300">Permissions (only enforced with Owner Password):</legend>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-printing" checked class="checkbox-style"> Print Document</label>
|
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-copying" checked class="checkbox-style"> Copy Content</label>
|
<input type="checkbox" id="allow-printing" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-modifying" checked class="checkbox-style"> Modify Document</label>
|
Allow Printing
|
||||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-annotating" checked class="checkbox-style"> Annotate & Comment</label>
|
</label>
|
||||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-filling-forms" checked class="checkbox-style"> Fill in Forms</label>
|
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-content-accessibility" checked class="checkbox-style"> Enable Content Accessibility</label>
|
<input type="checkbox" id="allow-copying" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||||
<label class="flex items-center gap-2"><input type="checkbox" id="allow-document-assembly" checked class="checkbox-style"> Assemble Document</label>
|
Allow Text/Image Extraction
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||||
|
<input type="checkbox" id="allow-modifying" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||||
|
Allow Modifications
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||||
|
<input type="checkbox" id="allow-annotating" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||||
|
Allow Annotations
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||||
|
<input type="checkbox" id="allow-filling-forms" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||||
|
Allow Form Filling
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||||
|
<input type="checkbox" id="allow-document-assembly" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||||
|
Allow Page Assembly
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 text-gray-300 cursor-pointer hover:text-white">
|
||||||
|
<input type="checkbox" id="allow-page-extraction" checked class="w-4 h-4 text-blue-600 bg-gray-700 border-gray-600 rounded">
|
||||||
|
Allow Page Extraction
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Save New Permissions</button>
|
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Apply Changes</button>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
'pdf-to-markdown': () => `
|
'pdf-to-markdown': () => `
|
||||||
@@ -1367,6 +1438,17 @@ export const toolTemplates = {
|
|||||||
'ocr-pdf': () => `
|
'ocr-pdf': () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">OCR PDF</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">OCR PDF</h2>
|
||||||
<p class="mb-6 text-gray-400">Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.</p>
|
<p class="mb-6 text-gray-400">Convert scanned PDFs into searchable documents. Select one or more languages present in your file for the best results.</p>
|
||||||
|
|
||||||
|
<div class="p-3 bg-gray-900 rounded-lg border border-gray-700 mb-6">
|
||||||
|
<p class="text-sm text-gray-300"><strong class="text-white">How it works:</strong></p>
|
||||||
|
<ul class="list-disc list-inside text-xs text-gray-400 mt-1 space-y-1">
|
||||||
|
<li><strong class="text-white">Extract Text:</strong> Uses Tesseract OCR to recognize text from scanned images or PDFs.</li>
|
||||||
|
<li><strong class="text-white">Searchable Output:</strong> Creates a new PDF with an invisible text layer, making your document fully searchable while preserving the original appearance.</li>
|
||||||
|
<li><strong class="text-white">Character Filtering:</strong> Use whitelists to filter out unwanted characters and improve accuracy for specific document types (invoices, forms, etc.).</li>
|
||||||
|
<li><strong class="text-white">Multi-language Support:</strong> Select multiple languages for documents containing mixed language content.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
${createFileInputHTML()}
|
${createFileInputHTML()}
|
||||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
|
|
||||||
@@ -1393,7 +1475,10 @@ export const toolTemplates = {
|
|||||||
|
|
||||||
<!-- Advanced settings section -->
|
<!-- Advanced settings section -->
|
||||||
<details class="bg-gray-900 border border-gray-700 rounded-lg p-3">
|
<details class="bg-gray-900 border border-gray-700 rounded-lg p-3">
|
||||||
<summary class="text-sm font-medium text-gray-300 cursor-pointer">Advanced Settings</summary>
|
<summary class="text-sm font-medium text-gray-300 cursor-pointer flex items-center justify-between">
|
||||||
|
<span>Advanced Settings (Recommended to improve accuracy)</span>
|
||||||
|
<i data-lucide="chevron-down" class="w-4 h-4 transition-transform details-icon"></i>
|
||||||
|
</summary>
|
||||||
<div class="mt-4 space-y-4">
|
<div class="mt-4 space-y-4">
|
||||||
<!-- Resolution Setting -->
|
<!-- Resolution Setting -->
|
||||||
<div>
|
<div>
|
||||||
@@ -1409,10 +1494,28 @@ export const toolTemplates = {
|
|||||||
<input type="checkbox" id="ocr-binarize" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600">
|
<input type="checkbox" id="ocr-binarize" class="w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600">
|
||||||
Binarize Image (Enhance Contrast for Clean Scans)
|
Binarize Image (Enhance Contrast for Clean Scans)
|
||||||
</label>
|
</label>
|
||||||
<!-- Character Whitelist -->
|
|
||||||
|
<!-- Character Whitelist Presets -->
|
||||||
|
<div>
|
||||||
|
<label for="whitelist-preset" class="block mb-1 text-xs font-medium text-gray-400">Character Whitelist Preset</label>
|
||||||
|
<select id="whitelist-preset" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm mb-2">
|
||||||
|
<option value="">None (All characters)</option>
|
||||||
|
<option value="alphanumeric">Alphanumeric + Basic Punctuation</option>
|
||||||
|
<option value="numbers-currency">Numbers + Currency Symbols</option>
|
||||||
|
<option value="letters-only">Letters Only (A-Z, a-z)</option>
|
||||||
|
<option value="numbers-only">Numbers Only (0-9)</option>
|
||||||
|
<option value="invoice">Invoice/Receipt (Numbers, $, ., -, /)</option>
|
||||||
|
<option value="forms">Forms (Alphanumeric + Common Symbols)</option>
|
||||||
|
<option value="custom">Custom...</option>
|
||||||
|
</select>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Only these characters will be recognized. Leave empty for all characters.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Character Whitelist Input -->
|
||||||
<div>
|
<div>
|
||||||
<label for="ocr-whitelist" class="block mb-1 text-xs font-medium text-gray-400">Character Whitelist (Optional)</label>
|
<label for="ocr-whitelist" class="block mb-1 text-xs font-medium text-gray-400">Character Whitelist (Optional)</label>
|
||||||
<input type="text" id="ocr-whitelist" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm" placeholder="e.g., abcdefghijklmnopqrstuvwxyz0123456789$.,">
|
<input type="text" id="ocr-whitelist" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2 text-sm" placeholder="e.g., abcdefghijklmnopqrstuvwxyz0123456789$.,">
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Only these characters will be recognized. Leave empty for all characters.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -1878,4 +1981,114 @@ export const toolTemplates = {
|
|||||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button>
|
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
'sanitize-pdf': () => `
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4">Sanitize PDF</h2>
|
||||||
|
<p class="mb-6 text-gray-400">Remove potentially sensitive or unnecessary information from your PDF before sharing. Select the items you want to remove.</p>
|
||||||
|
${createFileInputHTML()}
|
||||||
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
|
|
||||||
|
<div id="sanitize-pdf-options" class="hidden mt-6 space-y-4 p-4 bg-gray-900 border border-gray-700 rounded-lg">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-3">Sanitization Options</h3>
|
||||||
|
<div>
|
||||||
|
<strong class="font-semibold text-yellow-200">Note:</strong>
|
||||||
|
Removing <code class="bg-gray-700 px-1 rounded text-white">Embedded Fonts</code> may break text rendering! Text may not display correctly or at all. Only use if you're sure the PDF viewer has substitute fonts.
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h4 class="text-sm font-semibold text-gray-400 mb-2">Essential Security</h4>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="flatten-forms" name="sanitizeOption" value="flatten-forms" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white">Flatten Form Fields</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="remove-metadata" name="sanitizeOption" value="metadata" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white">Remove All Metadata</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="remove-annotations" name="sanitizeOption" value="annotations" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white">Remove Annotations</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="remove-javascript" name="sanitizeOption" value="javascript" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white">Remove JavaScript</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="remove-embedded-files" name="sanitizeOption" value="embeddedFiles" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white">Remove Embedded Files</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="remove-layers" name="sanitizeOption" value="layers" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white">Remove Layers (OCG)</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="remove-links" name="sanitizeOption" value="links" checked class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white">Remove External Links</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="text-sm font-semibold text-gray-400 mb-2">Additional Options</h4>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="remove-structure-tree" name="sanitizeOption" value="structure" class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white">Remove Structure Tree</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="remove-markinfo" name="sanitizeOption" value="markinfo" class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white">Remove Tagging Info</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center space-x-2 p-3 rounded-md bg-gray-800 hover:bg-gray-700 cursor-pointer">
|
||||||
|
<input type="checkbox" id="remove-fonts" name="sanitizeOption" value="fonts" class="w-5 h-5 text-indigo-600 bg-gray-700 border-gray-600 rounded focus:ring-indigo-500">
|
||||||
|
<span class="text-white text-sm">Remove Embedded Fonts</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="process-btn" class="btn-gradient w-full mt-6">Sanitize PDF & Download</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
'remove-restrictions': () => `
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4">Remove PDF Restrictions</h2>
|
||||||
|
<p class="mb-6 text-gray-400">Remove security restrictions and unlock PDF permissions for editing and printing.</p>
|
||||||
|
${createFileInputHTML()}
|
||||||
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
|
<div id="remove-restrictions-options" class="hidden space-y-4 mt-6">
|
||||||
|
<div class="p-4 bg-blue-900/20 border border-blue-500/30 text-blue-200 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-base mb-2"> How it Works </h3>
|
||||||
|
<p class="text-sm text-gray-300 mb-2">This operation will:</p>
|
||||||
|
<ul class="text-sm text-gray-300 list-disc list-inside space-y-1 ml-2">
|
||||||
|
<li>Remove all permission restrictions (printing, copying, editing)</li>
|
||||||
|
<li>Remove encryption even if the file is encrypted</li>
|
||||||
|
<li>Remove security restrictions associated with digitally signed PDF files (will make signature invalid)</li>
|
||||||
|
<li>Create a fully editable, unrestricted PDF</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="owner-password-remove" class="block mb-2 text-sm font-medium text-gray-300">Owner Password (if required)</label>
|
||||||
|
<input type="password" id="owner-password-remove" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5" placeholder="Leave empty if PDF has no password">
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Enter the owner password if the PDF is password-protected</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4 bg-red-900/20 border border-red-500/30 text-red-200 rounded-lg">
|
||||||
|
<h3 class="font-semibold text-base mb-2">Notice</h3>
|
||||||
|
<p class="text-sm text-gray-300 mb-2">This tool is intended for legitimate purposes only, such as:</p>
|
||||||
|
<ul class="text-sm text-gray-300 list-disc list-inside space-y-1 ml-2">
|
||||||
|
<li>Removing restrictions from PDFs you own or have permission to modify</li>
|
||||||
|
<li>Recovering access to a PDF when you legitimately forgot the password</li>
|
||||||
|
<li>Accessing content you legally purchased or created</li>
|
||||||
|
<li>Editing documents for authorized business purposes</li>
|
||||||
|
<li>Opening documents for legitimate archival, compliance, or recovery workflows</li>
|
||||||
|
<li class="font-semibold">Limitations: this tool can only remove restrictions from weakly protected PDFs or PDFs that do not have an owner password set. It cannot remove or bypass properly applied AES‑256 (256‑bit) encryption.</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-sm text-gray-300 mt-3 font-semibold">
|
||||||
|
Using this tool to bypass copyright protections, violate intellectual property rights, or access documents without authorization may be illegal in your jurisdiction. We are not liable for any misuse of this tool — if you're unsure, consult legal counsel or the document owner before proceeding.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button id="process-btn" class="btn-gradient w-full mt-6">Remove Restrictions & Download</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import createModule from '@neslinesli93/qpdf-wasm';
|
||||||
|
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||||
|
|
||||||
const STANDARD_SIZES = {
|
const STANDARD_SIZES = {
|
||||||
A4: { width: 595.28, height: 841.89 },
|
A4: { width: 595.28, height: 841.89 },
|
||||||
Letter: { width: 612, height: 792 },
|
Letter: { width: 612, height: 792 },
|
||||||
@@ -123,3 +126,54 @@ export function parsePageRanges(rangeString: any, totalPages: any) {
|
|||||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||||
return Array.from(indices).sort((a, b) => a - b);
|
return Array.from(indices).sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats an ISO 8601 date string (e.g., "2008-02-21T17:15:56-08:00")
|
||||||
|
* into a localized, human-readable string.
|
||||||
|
* @param {string} isoDateString - The ISO 8601 date string.
|
||||||
|
* @returns {string} A localized date and time string, or the original string if parsing fails.
|
||||||
|
*/
|
||||||
|
export function formatIsoDate(isoDateString) {
|
||||||
|
if (!isoDateString || typeof isoDateString !== 'string') {
|
||||||
|
return isoDateString; // Return original value if it's not a valid string
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const date = new Date(isoDateString);
|
||||||
|
// Check if the date object is valid
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return isoDateString; // Return original string if the date is invalid
|
||||||
|
}
|
||||||
|
return date.toLocaleString();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Could not parse ISO date:', e);
|
||||||
|
return isoDateString; // Return original string on any error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let qpdfInstance: any = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize qpdf-wasm singleton.
|
||||||
|
* Subsequent calls return the same instance.
|
||||||
|
*/
|
||||||
|
export async function initializeQpdf() {
|
||||||
|
if (qpdfInstance) return qpdfInstance;
|
||||||
|
|
||||||
|
showLoader('Initializing PDF 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 PDF engine. Please refresh the page and try again.'
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
return qpdfInstance;
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ describe('Tool Configuration Arrays', () => {
|
|||||||
|
|
||||||
it('should have the correct number of tools', () => {
|
it('should have the correct number of tools', () => {
|
||||||
// This acts as a snapshot test to catch unexpected additions/removals.
|
// This acts as a snapshot test to catch unexpected additions/removals.
|
||||||
expect(singlePdfLoadTools).toHaveLength(39);
|
expect(singlePdfLoadTools).toHaveLength(40);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not contain any duplicate tools', () => {
|
it('should not contain any duplicate tools', () => {
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ describe('reversePages - multi PDF support', () => {
|
|||||||
|
|
||||||
await reversePages();
|
await reversePages();
|
||||||
|
|
||||||
expect(helpers.downloadFile).toHaveBeenCalledWith(expect.any(Blob), 'reversed_pdfs.zip');
|
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(Blob),
|
||||||
|
'reversed_pdfs.zip'
|
||||||
|
);
|
||||||
expect(mockNewDoc.copyPages).toHaveBeenCalledTimes(2);
|
expect(mockNewDoc.copyPages).toHaveBeenCalledTimes(2);
|
||||||
expect(mockNewDoc.addPage).toHaveBeenCalled();
|
expect(mockNewDoc.addPage).toHaveBeenCalled();
|
||||||
expect(mockNewDoc.save).toHaveBeenCalledTimes(2);
|
expect(mockNewDoc.save).toHaveBeenCalledTimes(2);
|
||||||
@@ -77,38 +80,61 @@ describe('reversePages - multi PDF support', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle PDF creation errors', async () => {
|
it('should handle PDF creation errors', async () => {
|
||||||
vi.mocked(PDFLibDocument.create).mockRejectedValue(new Error('Create failed'));
|
vi.mocked(PDFLibDocument.create).mockRejectedValue(
|
||||||
|
new Error('Create failed')
|
||||||
|
);
|
||||||
state.files = [
|
state.files = [
|
||||||
{ name: 'x.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) },
|
{
|
||||||
|
name: 'x.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await reversePages();
|
await reversePages();
|
||||||
|
|
||||||
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.');
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
'Could not reverse the PDF pages.'
|
||||||
|
);
|
||||||
expect(ui.hideLoader).toHaveBeenCalled();
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle PDF processing errors', async () => {
|
it('should handle PDF processing errors', async () => {
|
||||||
mockNewDoc.copyPages.mockRejectedValue(new Error('Copy failed'));
|
mockNewDoc.copyPages.mockRejectedValue(new Error('Copy failed'));
|
||||||
state.files = [
|
state.files = [
|
||||||
{ name: 'y.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) },
|
{
|
||||||
|
name: 'y.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await reversePages();
|
await reversePages();
|
||||||
|
|
||||||
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.');
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
'Could not reverse the PDF pages.'
|
||||||
|
);
|
||||||
expect(ui.hideLoader).toHaveBeenCalled();
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle save errors', async () => {
|
it('should handle save errors', async () => {
|
||||||
mockNewDoc.save.mockRejectedValue(new Error('Save failed'));
|
mockNewDoc.save.mockRejectedValue(new Error('Save failed'));
|
||||||
state.files = [
|
state.files = [
|
||||||
{ name: 'z.pdf', type: 'application/pdf', arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)) },
|
{
|
||||||
|
name: 'z.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
await reversePages();
|
await reversePages();
|
||||||
|
|
||||||
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'Could not reverse the PDF pages.');
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
'Could not reverse the PDF pages.'
|
||||||
|
);
|
||||||
expect(ui.hideLoader).toHaveBeenCalled();
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { state, resetState } from '../js/state';
|
||||||
|
|
||||||
|
describe('Watermark Feature', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input type="text" id="watermark-text" value="Test Text" />
|
||||||
|
<input type="file" id="image-watermark-input" />
|
||||||
|
<input type="number" id="font-size" value="72" />
|
||||||
|
<input type="color" id="text-color" value="#000000" />
|
||||||
|
<input type="range" id="opacity-text" value="0.3" />
|
||||||
|
<input type="range" id="angle-text" value="0" />
|
||||||
|
<div id="text-watermark-options"></div>
|
||||||
|
<div id="image-watermark-options" class="hidden"></div>
|
||||||
|
<input type="radio" name="watermark-type" value="text" checked />
|
||||||
|
<input type="radio" name="watermark-type" value="image" />
|
||||||
|
<div id="file-display-area"></div>
|
||||||
|
<div id="file-controls" class="hidden"></div>
|
||||||
|
<button id="process-btn" disabled></button>
|
||||||
|
<input type="file" id="file-input" />
|
||||||
|
<div id="tool-content"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
state.activeTool = 'add-watermark';
|
||||||
|
state.files = [];
|
||||||
|
state.pdfDoc = null;
|
||||||
|
state.pdfPages = [];
|
||||||
|
state.currentPdfUrl = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resetState', () => {
|
||||||
|
it('should clear watermark text input after reset', () => {
|
||||||
|
const watermarkText = document.getElementById(
|
||||||
|
'watermark-text'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
watermarkText.value = 'CONFIDENTIAL';
|
||||||
|
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
expect(state.activeTool).toBe(null);
|
||||||
|
expect(state.files).toEqual([]);
|
||||||
|
expect(state.pdfDoc).toBe(null);
|
||||||
|
expect(state.pdfPages).toEqual([]);
|
||||||
|
expect(state.currentPdfUrl).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset state files array', () => {
|
||||||
|
state.files = [new File(['content'], 'test.pdf')];
|
||||||
|
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
expect(state.files).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset activeTool to null', () => {
|
||||||
|
state.activeTool = 'add-watermark';
|
||||||
|
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
expect(state.activeTool).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset pdfDoc to null', () => {
|
||||||
|
state.pdfDoc = {} as any;
|
||||||
|
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
expect(state.pdfDoc).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset pdfPages array', () => {
|
||||||
|
state.pdfPages = [1, 2, 3] as any;
|
||||||
|
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
expect(state.pdfPages).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset currentPdfUrl to null', () => {
|
||||||
|
state.currentPdfUrl = 'blob:http://example.com/123';
|
||||||
|
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
expect(state.currentPdfUrl).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear tool-content innerHTML', () => {
|
||||||
|
const toolContent = document.getElementById('tool-content');
|
||||||
|
if (toolContent) toolContent.innerHTML = '<div>Some content</div>';
|
||||||
|
|
||||||
|
resetState();
|
||||||
|
|
||||||
|
expect(toolContent?.innerHTML).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Watermark Type Toggle', () => {
|
||||||
|
it('should show text options by default', () => {
|
||||||
|
const textOptions = document.getElementById('text-watermark-options');
|
||||||
|
const imageOptions = document.getElementById('image-watermark-options');
|
||||||
|
|
||||||
|
expect(imageOptions?.classList.contains('hidden')).toBe(true);
|
||||||
|
expect(textOptions?.classList.contains('hidden')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have text radio checked by default', () => {
|
||||||
|
const textRadio = document.querySelector(
|
||||||
|
'input[name="watermark-type"][value="text"]'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const imageRadio = document.querySelector(
|
||||||
|
'input[name="watermark-type"][value="image"]'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
|
expect(textRadio.checked).toBe(true);
|
||||||
|
expect(imageRadio.checked).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Watermark Input Validation', () => {
|
||||||
|
it('should accept valid font size', () => {
|
||||||
|
const fontSize = document.getElementById('font-size') as HTMLInputElement;
|
||||||
|
fontSize.value = '48';
|
||||||
|
|
||||||
|
expect(parseInt(fontSize.value)).toBeGreaterThan(0);
|
||||||
|
expect(parseInt(fontSize.value)).toBeLessThanOrEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have default font size of 72', () => {
|
||||||
|
const fontSize = document.getElementById('font-size') as HTMLInputElement;
|
||||||
|
|
||||||
|
expect(parseInt(fontSize.value)).toBe(72);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid opacity range', () => {
|
||||||
|
const opacity = document.getElementById(
|
||||||
|
'opacity-text'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
opacity.value = '0.5';
|
||||||
|
|
||||||
|
const opacityValue = parseFloat(opacity.value);
|
||||||
|
expect(opacityValue).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(opacityValue).toBeLessThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have default opacity of 0.3', () => {
|
||||||
|
const opacity = document.getElementById(
|
||||||
|
'opacity-text'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
|
expect(parseFloat(opacity.value)).toBe(0.3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid angle range', () => {
|
||||||
|
const angle = document.getElementById('angle-text') as HTMLInputElement;
|
||||||
|
angle.value = '45';
|
||||||
|
|
||||||
|
const angleValue = parseInt(angle.value);
|
||||||
|
expect(angleValue).toBeGreaterThanOrEqual(-180);
|
||||||
|
expect(angleValue).toBeLessThanOrEqual(180);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have default angle of 0', () => {
|
||||||
|
const angle = document.getElementById('angle-text') as HTMLInputElement;
|
||||||
|
|
||||||
|
expect(parseInt(angle.value)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid hex color', () => {
|
||||||
|
const textColor = document.getElementById(
|
||||||
|
'text-color'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
textColor.value = '#FF5733';
|
||||||
|
|
||||||
|
expect(textColor.value).toMatch(/^#[0-9A-F]{6}$/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have default color of black', () => {
|
||||||
|
const textColor = document.getElementById(
|
||||||
|
'text-color'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
|
expect(textColor.value).toBe('#000000');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('State Management', () => {
|
||||||
|
it('should maintain activeTool as add-watermark', () => {
|
||||||
|
expect(state.activeTool).toBe('add-watermark');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with empty files array', () => {
|
||||||
|
expect(state.files).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow adding files to state', () => {
|
||||||
|
const file = new File(['content'], 'test.pdf', {
|
||||||
|
type: 'application/pdf',
|
||||||
|
});
|
||||||
|
state.files.push(file);
|
||||||
|
|
||||||
|
expect(state.files.length).toBe(1);
|
||||||
|
expect(state.files[0].name).toBe('test.pdf');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('UI Elements State', () => {
|
||||||
|
it('should have process button disabled by default', () => {
|
||||||
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
|
expect(processBtn.disabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have file controls hidden by default', () => {
|
||||||
|
const fileControls = document.getElementById('file-controls');
|
||||||
|
|
||||||
|
expect(fileControls?.classList.contains('hidden')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have image watermark options hidden by default', () => {
|
||||||
|
const imageOptions = document.getElementById('image-watermark-options');
|
||||||
|
|
||||||
|
expect(imageOptions?.classList.contains('hidden')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+83
-43
@@ -11,54 +11,94 @@
|
|||||||
<link href="/src/css/styles.css" rel="stylesheet" />
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl ml-2"> <a href="index.html">BentoPDF</a> </span>
|
id="home-logo"
|
||||||
</div>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Desktop Navigation -->
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Hamburger Button -->
|
<!-- Mobile Hamburger Button -->
|
||||||
<div class="md:hidden flex items-center">
|
<div class="md:hidden flex items-center">
|
||||||
<button id="mobile-menu-button" type="button"
|
<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"
|
id="mobile-menu-button"
|
||||||
aria-controls="mobile-menu" aria-expanded="false">
|
type="button"
|
||||||
<span class="sr-only">Open main menu</span>
|
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"
|
||||||
<!-- Hamburger Icon -->
|
aria-controls="mobile-menu"
|
||||||
<svg id="menu-icon" class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
aria-expanded="false"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<span class="sr-only">Open main menu</span>
|
||||||
d="M4 6h16M4 12h16M4 18h16" />
|
<!-- Hamburger Icon -->
|
||||||
</svg>
|
<svg
|
||||||
<!-- Close Icon -->
|
id="menu-icon"
|
||||||
<svg id="close-icon" class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none"
|
class="block h-6 w-6"
|
||||||
viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
fill="none"
|
||||||
d="M6 18L18 6M6 6l12 12" />
|
viewBox="0 0 24 24"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
</button>
|
aria-hidden="true"
|
||||||
</div>
|
>
|
||||||
</div>
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mobile Menu Dropdown -->
|
<!-- Mobile Menu Dropdown -->
|
||||||
<div id="mobile-menu" class="hidden md:hidden bg-gray-800 border-t border-gray-700">
|
<div
|
||||||
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
id="mobile-menu"
|
||||||
<a href="index.html" class="mobile-nav-link">Home</a>
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
<a href="./about.html" class="mobile-nav-link">About</a>
|
>
|
||||||
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
<a href="index.html#tools-header" class="mobile-nav-link">All Tools</a>
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div id="app" class="container mx-auto p-4 md:p-8">
|
<div id="app" class="container mx-auto p-4 md:p-8">
|
||||||
<section class="max-w-4xl mx-auto py-12">
|
<section class="max-w-4xl mx-auto py-12">
|
||||||
|
|||||||
Reference in new issue
Block a user