mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-10 17: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 | ||
|
|
59b351eee4 | ||
|
|
48fe49ac37 | ||
|
|
ccb1b119c5 | ||
|
|
6971c18e5e | ||
|
|
8a72063ef7 | ||
|
|
4e6cc77a28 | ||
|
|
f037e27535 | ||
|
|
6270080045 | ||
|
|
f9a06b54f5 | ||
|
|
584f2f09d1 | ||
|
|
8ac4793f9b | ||
|
|
d163bd29e6 | ||
|
|
34a00ad74a | ||
|
|
797d8f5b9d | ||
|
|
ef7b9d0365 | ||
|
|
6bfcb30592 | ||
|
|
a605e56834 | ||
|
|
2e2c634b41 | ||
|
|
caf23b0ab9 | ||
|
|
88e05e6713 | ||
|
|
b3ab445538 | ||
|
|
ddeebc8142 | ||
|
|
8a494c0a01 | ||
|
|
f11efe7278 | ||
|
|
8e549b7304 | ||
|
|
574ed4bd36 | ||
|
|
7d1a1c9064 | ||
|
|
560d45d290 | ||
|
|
a48ce08d39 | ||
|
|
f8579fb291 | ||
|
|
f9ef634b6f | ||
|
|
6841b38eac | ||
|
|
d98e18b51f | ||
|
|
cc83d7951f | ||
|
|
298135ab0d | ||
|
|
a61ec759ec | ||
|
|
d33a02b1a5 | ||
|
|
f1d830d81d | ||
|
|
87c191213c | ||
|
|
2e53b68631 | ||
|
|
17557d294b | ||
|
|
886b81eb4f | ||
|
|
70030fedda | ||
|
|
48baad9bf9 | ||
|
|
a82148c253 | ||
|
|
39080c800f | ||
|
|
d4633e1429 | ||
|
|
1cf40d9328 |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
github: alam00000
|
||||||
|
ko_fi: alio01
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
name: '🐛 Bug / 💡 Feature / ❓ Question'
|
||||||
|
about: 'Report a bug, request a feature, or ask a question about BentoPDF'
|
||||||
|
title: '(Bug) <short title>, (Feature) <short title>, or (Question) <short title>'
|
||||||
|
labels: ['needs triage']
|
||||||
|
assignees: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type of Issue
|
||||||
|
|
||||||
|
Please check one:
|
||||||
|
|
||||||
|
- [ ] 🐛 Bug Report <!-- Label: bug -->
|
||||||
|
- [ ] 💡 Feature Request <!-- Label: feature -->
|
||||||
|
- [ ] ❓ Question / Help <!-- Label: question -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Provide a clear and concise description of the issue, feature request, or question.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Steps to Reproduce (for Bugs)
|
||||||
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Run '...'
|
||||||
|
3. Observe error: '...'
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
Describe what you expected BentoPDF to do.
|
||||||
|
|
||||||
|
**Actual Behavior:**
|
||||||
|
Describe what actually happened, including error messages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Request Details (if applicable)
|
||||||
|
|
||||||
|
- What functionality are you requesting?
|
||||||
|
- Why is this useful?
|
||||||
|
- Any example or context to illustrate it?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Question Details (if applicable)
|
||||||
|
|
||||||
|
- What is your question?
|
||||||
|
- What have you tried so far?
|
||||||
|
- Any relevant code snippet or scenario?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Screenshots / Logs (if applicable)
|
||||||
|
|
||||||
|
Attach any screenshots, logs, or stack traces that help explain the problem or question.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- **OS:** (e.g., macOS 14.0 / Ubuntu 22.04 / Windows 11)
|
||||||
|
- **Dependencies / setup details (if any):**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💭 Additional Context
|
||||||
|
|
||||||
|
Any other information, suggestions, or references that might help maintainers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
✅ **Title Format Reminder:**
|
||||||
|
|
||||||
|
- `(Bug) Text alignment incorrect on multi-line paragraphs`
|
||||||
|
- `(Feature) Add support for custom PDF metadata`
|
||||||
|
- `(Question) How to embed custom fonts?`
|
||||||
|
|
||||||
|
---
|
||||||
@@ -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'
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
### Description
|
||||||
|
|
||||||
|
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||||
|
|
||||||
|
Fixes # (issue)
|
||||||
|
|
||||||
|
### Type of change
|
||||||
|
|
||||||
|
Please delete options that are not relevant.
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] This change requires a documentation update
|
||||||
|
|
||||||
|
### 🧪 How Has This Been Tested?
|
||||||
|
|
||||||
|
Please describe **how you tested your changes** so that maintainers can verify them. Include enough detail for someone else to reproduce your tests.
|
||||||
|
|
||||||
|
**Checklist:**
|
||||||
|
|
||||||
|
- [ ] Verified output manually
|
||||||
|
- [ ] Tested with relevant sample documents or data
|
||||||
|
- [ ] Wrote Vite Test Case (if applicable)
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
|
||||||
|
- What the tests should produce (e.g., correct PDF rendering, no errors).
|
||||||
|
|
||||||
|
**Actual Results:**
|
||||||
|
|
||||||
|
- What you observed when running the tests.
|
||||||
|
|
||||||
|
### Checklist:
|
||||||
|
|
||||||
|
- [ ] My code follows the style guidelines of this project
|
||||||
|
- [ ] I have performed a self-review of my own code
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] I have made corresponding changes to the documentation
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||||
|
- [ ] New and existing unit tests pass locally with my changes
|
||||||
|
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||||
@@ -1,30 +1,60 @@
|
|||||||
name: build docker image and push to dockerhub
|
name: Build and Push Docker Images (Default + Simple Mode)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- 'main'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build-and-push:
|
docker-build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: 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
|
||||||
-
|
- name: Set up Docker Buildx
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
-
|
- name: Login to DockerHub
|
||||||
name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
-
|
- name: Extract version from tag
|
||||||
name: Build and push
|
id: version
|
||||||
|
run: |
|
||||||
|
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version_without_v=${VERSION#v}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "version=latest" >> $GITHUB_OUTPUT
|
||||||
|
echo "version_without_v=latest" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build and push ${{ matrix.mode.name }} image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: bentopdf/bentopdf:latest
|
build-args: |
|
||||||
|
SIMPLE_MODE=${{ matrix.mode.simple_mode }}
|
||||||
|
tags: |
|
||||||
|
bentopdf/bentopdf${{ matrix.mode.suffix }}:latest
|
||||||
|
bentopdf/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version_without_v }}
|
||||||
|
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
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
+35
-1
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# Contributing to BentoPDF
|
# Contributing to BentoPDF
|
||||||
|
|
||||||
First off, thank you for considering contributing to **BentoPDF**! Your help makes this project better for everyone.
|
First off, thank you for considering contributing to **BentoPDF**! Your help makes this project better for everyone.
|
||||||
@@ -19,9 +18,39 @@ You can contribute in several ways:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Issue Template
|
||||||
|
|
||||||
|
When reporting bugs, requesting features, or asking questions, please use our [issue template](.github/ISSUE_TEMPLATE/bug_feature_question.md). The template will automatically appear when you create a new issue.
|
||||||
|
|
||||||
|
**Our issue template helps you provide:**
|
||||||
|
|
||||||
|
- Clear categorization (Bug, Feature Request, or Question)
|
||||||
|
- Steps to reproduce (for bugs)
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Environment details
|
||||||
|
- Screenshots or logs
|
||||||
|
|
||||||
|
**Title Format Examples:**
|
||||||
|
|
||||||
|
- `(Bug) Text alignment incorrect on multi-line paragraphs`
|
||||||
|
- `(Feature) Add support for custom PDF metadata`
|
||||||
|
- `(Question) How to embed custom fonts?`
|
||||||
|
|
||||||
|
### Pull Request Template
|
||||||
|
|
||||||
|
When submitting code contributions, please use our [pull request template](.github/pull_request_template.md). The template will automatically appear when you create a new PR.
|
||||||
|
|
||||||
|
**Key requirements:**
|
||||||
|
|
||||||
|
- Link to the related issue (e.g., `Fixes #123`)
|
||||||
|
- Describe the type of change (bug fix, feature, breaking change)
|
||||||
|
- Explain how you tested your changes
|
||||||
|
- Complete the checklist before submitting
|
||||||
|
|
||||||
## 2. Getting Started with Code Contributions
|
## 2. Getting Started with Code Contributions
|
||||||
|
|
||||||
1. **Fork the Repository**
|
1. **Fork the Repository**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/alam00000/bentopdf.git
|
git clone https://github.com/alam00000/bentopdf.git
|
||||||
cd bento-pdf
|
cd bento-pdf
|
||||||
@@ -29,6 +58,7 @@ You can contribute in several ways:
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Create a New Branch**
|
2. **Create a New Branch**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout -b feature/my-new-feature
|
git checkout -b feature/my-new-feature
|
||||||
```
|
```
|
||||||
@@ -39,20 +69,24 @@ You can contribute in several ways:
|
|||||||
- Update or add tests if applicable.
|
- Update or add tests if applicable.
|
||||||
|
|
||||||
4. **Run Tests**
|
4. **Run Tests**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run test
|
npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Commit Your Changes**
|
5. **Commit Your Changes**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Add a meaningful commit message"
|
git commit -m "Add a meaningful commit message"
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Push and Submit a Pull Request**
|
6. **Push and Submit a Pull Request**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git push origin feature/my-new-feature
|
git push origin feature/my-new-feature
|
||||||
```
|
```
|
||||||
|
|
||||||
- Open a pull request on GitHub and provide a clear description of your changes.
|
- Open a pull request on GitHub and provide a clear description of your changes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ RUN npm ci
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build without type checking (vite build only)
|
# Build without type checking (vite build only)
|
||||||
|
# Pass SIMPLE_MODE environment variable if provided
|
||||||
|
ARG SIMPLE_MODE=false
|
||||||
|
ENV SIMPLE_MODE=$SIMPLE_MODE
|
||||||
RUN npm run build -- --mode production
|
RUN npm run build -- --mode production
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
# BentoPDF
|
# BentoPDF
|
||||||
|
|
||||||

|
|
||||||
[](https://ko-fi.com/alio0)
|
|
||||||
|
|
||||||
**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://github.com/sponsors/alam00000)
|
||||||
|
|
||||||
|
## ⭐ Stargazers over time
|
||||||
|
|
||||||
|
[](https://star-history.com/#alam00000/bentopdf&Date)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Why BentoPDF?
|
## ✨ Why BentoPDF?
|
||||||
@@ -21,8 +25,9 @@
|
|||||||
BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
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. |
|
||||||
@@ -31,10 +36,13 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
|||||||
| **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
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :--- | :--- |
|
| :--------------------- | :---------------------------------------------------------- |
|
||||||
| **PDF Editor** | A comprehensive editor to modify your PDFs. |
|
| **PDF Editor** | A comprehensive editor to modify your PDFs. |
|
||||||
| **Add Page Numbers** | Easily add page numbers with customizable formatting. |
|
| **Add Page Numbers** | Easily add page numbers with customizable formatting. |
|
||||||
| **Add Watermark** | Add text or image watermarks to protect your documents. |
|
| **Add Watermark** | Add text or image watermarks to protect your documents. |
|
||||||
@@ -46,24 +54,28 @@ 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
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :--- | :--- |
|
| :------------------ | :-------------------------------------------------------------- |
|
||||||
| **Image to PDF** | Convert JPG, PNG, WebP, SVG, BMP, HEIC, and TIFF images to PDF. |
|
| **Image to PDF** | Convert JPG, PNG, WebP, SVG, BMP, HEIC, and TIFF images to PDF. |
|
||||||
| **Markdown to PDF** | Convert `.md` files into professional PDF documents. |
|
| **Markdown to PDF** | Convert `.md` files into professional PDF documents. |
|
||||||
| **Text to PDF** | Convert plain text files into a PDF. |
|
| **Text to PDF** | Convert plain text files into a PDF. |
|
||||||
|
|
||||||
### Convert from PDF
|
### Convert from PDF
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :--- | :--- |
|
| :------------------- | :----------------------------------------------------------------------------- |
|
||||||
| **PDF to Image** | Convert PDF pages to JPG, PNG, WebP, BMP, or TIFF formats. |
|
| **PDF to Image** | Convert PDF pages to JPG, PNG, WebP, BMP, or TIFF formats. |
|
||||||
| **PDF to Greyscale** | Convert a color PDF into a black-and-white version. |
|
| **PDF to Greyscale** | Convert a color PDF into a black-and-white version. |
|
||||||
| **OCR PDF** | Make scanned PDFs searchable and copyable using Optical Character Recognition. |
|
| **OCR PDF** | Make scanned PDFs searchable and copyable using Optical Character Recognition. |
|
||||||
|
|
||||||
### Secure & Optimize PDFs
|
### Secure & Optimize PDFs
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :--- | :--- |
|
| :--------------------- | :----------------------------------------------------------------- |
|
||||||
| **Compress PDF** | Reduce file size while maintaining quality. |
|
| **Compress PDF** | Reduce file size while maintaining quality. |
|
||||||
| **Repair PDF** | Attempt to repair and recover data from a corrupted PDF. |
|
| **Repair PDF** | Attempt to repair and recover data from a corrupted PDF. |
|
||||||
| **Encrypt PDF** | Add a password to protect your PDF from unauthorized access. |
|
| **Encrypt PDF** | Add a password to protect your PDF from unauthorized access. |
|
||||||
@@ -73,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. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -81,30 +95,96 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
|||||||
You can run BentoPDF locally for development or personal use.
|
You can run BentoPDF locally for development or personal use.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) (v18 or higher recommended)
|
- [Node.js](https://nodejs.org/) (v18 or higher recommended)
|
||||||
- [npm](https://www.npmjs.com/) (or yarn/pnpm)
|
- [npm](https://www.npmjs.com/) (or yarn/pnpm)
|
||||||
- [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/install/) (for containerized setup)
|
- [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/install/) (for containerized setup)
|
||||||
|
|
||||||
### 🚀 Run BentoPDF with Docker (Quick Start)
|
### 🚀 Quick Start with Docker
|
||||||
|
|
||||||
You can run BentoPDF directly from Docker Hub without cloning the repository:
|
You can run BentoPDF directly from Docker Hub without cloning the repository:
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull bentopdf/bentopdf:latest
|
|
||||||
docker run -p 3000:80 bentopdf/bentopdf:latest
|
docker run -p 3000:80 bentopdf/bentopdf:latest
|
||||||
```
|
```
|
||||||
Open your browser at: http://localhost:3000.
|
|
||||||
|
Open your browser at: http://localhost:3000
|
||||||
|
|
||||||
This is the fastest way to try BentoPDF without setting up a development environment.
|
This is the fastest way to try BentoPDF without setting up a development environment.
|
||||||
|
|
||||||
### 🚀 Installation with npm
|
### 🚀 Run with Docker Compose (Recommended)
|
||||||
|
|
||||||
|
For a more robust setup with auto-restart capabilities:
|
||||||
|
|
||||||
|
1. **Download the repo and create a `docker-compose.yml` file or use the one given in repo**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
image: bentopdf/bentopdf:latest
|
||||||
|
container_name: bentopdf
|
||||||
|
ports:
|
||||||
|
- '3000:80'
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start the application**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:3000`.
|
||||||
|
|
||||||
|
### 🏢 Simple Mode for Internal Use
|
||||||
|
|
||||||
|
For organizations that want a clean, distraction-free interface focused solely on PDF tools, BentoPDF supports a **Simple Mode** that hides all branding and marketing content.
|
||||||
|
|
||||||
|
**What Simple Mode does:**
|
||||||
|
|
||||||
|
- Hides navigation, hero section, features, FAQ, testimonials, and footer
|
||||||
|
- Shows only the essential PDF tools
|
||||||
|
- Updates page title to "PDF Tools"
|
||||||
|
- Perfect for internal company tools and educational institutions
|
||||||
|
|
||||||
|
For more details, see [SIMPLE_MODE.md](SIMPLE_MODE.md).
|
||||||
|
|
||||||
|
### 📦 Version Management
|
||||||
|
|
||||||
|
BentoPDF supports semantic versioning with multiple Docker tags:
|
||||||
|
|
||||||
|
- **Latest**: `bentopdf/bentopdf:latest`
|
||||||
|
- **Specific Version**: `bentopdf/bentopdf:1.0.0`
|
||||||
|
- **Version with Prefix**: `bentopdf/bentopdf:v1.0.0`
|
||||||
|
|
||||||
|
#### Quick Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Release a patch version (0.0.1 → 0.0.2)
|
||||||
|
npm run release
|
||||||
|
|
||||||
|
# Release a minor version (0.0.1 → 0.1.0)
|
||||||
|
npm run release:minor
|
||||||
|
|
||||||
|
# Release a major version (0.0.1 → 1.0.0)
|
||||||
|
npm run release:major
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed release instructions, see [RELEASE.md](RELEASE.md).
|
||||||
|
|
||||||
|
### 🚀 Development Setup
|
||||||
|
|
||||||
|
#### Option 1: Run with npm
|
||||||
|
|
||||||
1. **Clone the Repository**:
|
1. **Clone the Repository**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/alam00000/bentopdf.git
|
git clone https://github.com/alam00000/bentopdf.git
|
||||||
cd bentopdf
|
cd bentopdf
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install Dependencies**:
|
2. **Install Dependencies**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
@@ -115,31 +195,33 @@ This is the fastest way to try BentoPDF without setting up a development environ
|
|||||||
```
|
```
|
||||||
The application will be available at `http://localhost:5173`.
|
The application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
### 🚀 Running with Docker Compose Locally
|
#### Option 2: Build and Run with Docker Compose
|
||||||
|
|
||||||
1. **Clone the Repository**:
|
1. **Clone the Repository**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/alam00000/bentopdf.git
|
git clone https://github.com/alam00000/bentopdf.git
|
||||||
cd bentopdf
|
cd bentopdf
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run with Docker Compose**:
|
2. **Run with Docker Compose**:
|
||||||
Create a `docker-compose.yml` file with the content provided in the repository or use the existing one. Then, run:
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The application will be available at `http://localhost:3000`.
|
The application will be available at `http://localhost:3000`.
|
||||||
|
|
||||||
> **Note:** After making any local changes to the code, it’s recommended to rebuild the Docker image using:
|
> **Note:** After making any local changes to the code, rebuild the Docker image using:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up --build -d
|
docker-compose -f docker-compose.dev.yml up --build -d
|
||||||
```
|
```
|
||||||
This ensures your latest changes are applied inside the container
|
|
||||||
|
This ensures your latest changes are applied inside the container.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 🛠️ Tech Stack & Background
|
## 🛠️ Tech Stack & Background
|
||||||
|
|
||||||
BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript**. As the project grew, it was migrated to a modern stack for better maintainability and scalability:
|
BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript**. As the project grew, it was migrated to a modern stack for better maintainability and scalability:
|
||||||
@@ -155,20 +237,21 @@ BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript*
|
|||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
### Planned Features:
|
### Planned Features:
|
||||||
|
|
||||||
- **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.
|
||||||
|
- **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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
We welcome contributions from the community! Here’s how you can get started:
|
We welcome contributions from the community! Here's how you can get started:
|
||||||
|
|
||||||
1. **Fork the repository** and create your branch from `main`.
|
1. **Fork the repository** and create your branch from `main`.
|
||||||
2. Follow the **Getting Started** steps to set up your local environment.
|
2. Follow the **Getting Started** steps to set up your local environment.
|
||||||
@@ -181,15 +264,16 @@ Have an idea for a new tool or an improvement? [Open an issue](https://github.co
|
|||||||
|
|
||||||
## Special Thanks
|
## Special Thanks
|
||||||
|
|
||||||
BentoPDF wouldn’t be possible without the amazing open-source tools and libraries that power it. We’d like to extend our heartfelt thanks to the creators and maintainers of:
|
BentoPDF wouldn't be possible without the amazing open-source tools and libraries that power it. We'd like to extend our heartfelt thanks to the creators and maintainers of:
|
||||||
|
|
||||||
- **[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!
|
||||||
|
|
||||||
|
|||||||
+317
@@ -0,0 +1,317 @@
|
|||||||
|
# 🚀 Release Process - Real-World Scenarios
|
||||||
|
|
||||||
|
## 📋 Common Release Scenarios
|
||||||
|
|
||||||
|
### **Scenario 1: I Just Finished a Feature and Want to Release**
|
||||||
|
|
||||||
|
**Situation:** You've completed a new feature, tested it locally, and want to release it.
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git status
|
||||||
|
On branch main
|
||||||
|
Your branch is up to date with 'origin/main'.
|
||||||
|
|
||||||
|
Changes not staged for commit:
|
||||||
|
modified: src/js/new-feature.js
|
||||||
|
modified: src/css/styles.css
|
||||||
|
modified: README.md
|
||||||
|
|
||||||
|
Untracked files:
|
||||||
|
src/js/feature-helper.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Commit your feature changes
|
||||||
|
git add .
|
||||||
|
git commit -m "Add new PDF watermark feature"
|
||||||
|
|
||||||
|
# 2. Choose your release type and run
|
||||||
|
npm run release # Patch: 1.0.0 → 1.0.1 (bug fixes, small improvements)
|
||||||
|
npm run release:minor # Minor: 1.0.0 → 1.1.0 (new features, backward compatible)
|
||||||
|
npm run release:major # Major: 1.0.0 → 2.0.0 (breaking changes)
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Happens:**
|
||||||
|
|
||||||
|
- ✅ Your feature commit stays as-is
|
||||||
|
- ✅ Version gets bumped in `package.json`
|
||||||
|
- ✅ New release commit is created
|
||||||
|
- ✅ Git tag is created (e.g., `v1.0.1`)
|
||||||
|
- ✅ Everything gets pushed to GitHub
|
||||||
|
- ✅ Docker image gets built and published
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 2: I Have Uncommitted Changes and Want to Release**
|
||||||
|
|
||||||
|
**Situation:** You have local changes but haven't committed them yet.
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git status
|
||||||
|
Changes not staged for commit:
|
||||||
|
modified: package.json
|
||||||
|
modified: src/js/main.js
|
||||||
|
modified: README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ This Will Fail:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release
|
||||||
|
# Error: Your local changes would be overwritten by merge
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Solution Options:**
|
||||||
|
|
||||||
|
**Option A: Commit Everything First (Recommended)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Add new features and improvements"
|
||||||
|
npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Stash Changes Temporarily**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git stash
|
||||||
|
npm run release
|
||||||
|
git stash pop # Restore your changes after release
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C: Commit Only What's Needed**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add package.json src/js/main.js
|
||||||
|
git commit -m "Add core improvements"
|
||||||
|
npm run release
|
||||||
|
git add README.md
|
||||||
|
git commit -m "Update documentation"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 3: I Want to Release a Hotfix**
|
||||||
|
|
||||||
|
**Situation:** There's a critical bug in production that needs immediate fixing.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Fix the bug
|
||||||
|
git add src/js/bug-fix.js
|
||||||
|
git commit -m "Fix critical PDF rendering issue"
|
||||||
|
|
||||||
|
# 2. Release as patch (bug fix)
|
||||||
|
npm run release
|
||||||
|
# This creates: 1.0.0 → 1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
|
||||||
|
- ✅ Bug fix gets released immediately
|
||||||
|
- ✅ Docker image with fix is available
|
||||||
|
- ✅ Users can pull the fixed version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 4: I Want to Release a Major Update**
|
||||||
|
|
||||||
|
**Situation:** You've added significant new features that might break existing functionality.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Commit all your changes
|
||||||
|
git add .
|
||||||
|
git commit -m "Add major PDF editing features and API changes"
|
||||||
|
|
||||||
|
# 2. Release as major version
|
||||||
|
npm run release:major
|
||||||
|
# This creates: 1.0.0 → 2.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
|
||||||
|
- ✅ Major version bump indicates breaking changes
|
||||||
|
- ✅ Users know to check compatibility
|
||||||
|
- ✅ Both old and new versions available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 5: I Want to Release Multiple Features at Once**
|
||||||
|
|
||||||
|
**Situation:** You've been working on multiple features and want to release them together.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Commit all features
|
||||||
|
git add .
|
||||||
|
git commit -m "Add multiple PDF tools: watermark, encryption, and compression"
|
||||||
|
|
||||||
|
# 2. Choose appropriate release type
|
||||||
|
npm run release:minor # For new features (1.0.0 → 1.1.0)
|
||||||
|
# OR
|
||||||
|
npm run release:major # For breaking changes (1.0.0 → 2.0.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 6: I Want to Test the Release Process**
|
||||||
|
|
||||||
|
**Situation:** You want to test the release system without affecting production.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Make a small test change
|
||||||
|
echo "// Test comment" >> src/js/main.js
|
||||||
|
git add src/js/main.js
|
||||||
|
git commit -m "Test release process"
|
||||||
|
|
||||||
|
# 2. Run patch release
|
||||||
|
npm run release
|
||||||
|
# This creates: 1.0.0 → 1.0.1
|
||||||
|
|
||||||
|
# 3. Verify everything works
|
||||||
|
# Check GitHub Actions, Docker Hub, etc.
|
||||||
|
|
||||||
|
# 4. If you want to undo the test release
|
||||||
|
git tag -d v1.0.1
|
||||||
|
git push origin :refs/tags/v1.0.1
|
||||||
|
git reset --hard HEAD~1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Release Type Guidelines**
|
||||||
|
|
||||||
|
| Scenario | Command | Version Change | When to Use |
|
||||||
|
| ------------------- | ----------------------- | --------------- | ------------------------------------ |
|
||||||
|
| **Bug Fix** | `npm run release` | `1.0.0 → 1.0.1` | Fixing bugs, small improvements |
|
||||||
|
| **New Feature** | `npm run release:minor` | `1.0.0 → 1.1.0` | Adding features, backward compatible |
|
||||||
|
| **Breaking Change** | `npm run release:major` | `1.0.0 → 2.0.0` | API changes, major rewrites |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 **What Happens After You Run a Release Command**
|
||||||
|
|
||||||
|
### **Immediate Actions (Local):**
|
||||||
|
|
||||||
|
1. **Version Update**: `package.json` version gets bumped
|
||||||
|
2. **Git Commit**: New commit created with "Release vX.X.X"
|
||||||
|
3. **Git Tag**: Tag created (e.g., `v1.0.1`)
|
||||||
|
4. **Git Push**: Everything pushed to GitHub
|
||||||
|
|
||||||
|
### **Automatic Actions (GitHub):**
|
||||||
|
|
||||||
|
1. **GitHub Actions Triggered**: Workflow starts building Docker image
|
||||||
|
2. **Docker Build**: Multi-architecture image created
|
||||||
|
3. **Docker Push**: Images pushed to Docker Hub with tags:
|
||||||
|
- `bentopdf/bentopdf:latest`
|
||||||
|
- `bentopdf/bentopdf:1.0.1`
|
||||||
|
- `bentopdf/bentopdf:v1.0.1`
|
||||||
|
|
||||||
|
### **End Result:**
|
||||||
|
|
||||||
|
Users can immediately pull your new version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull bentopdf/bentopdf:1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 **Before You Release - Prerequisites**
|
||||||
|
|
||||||
|
### **1. Docker Hub Credentials Setup**
|
||||||
|
|
||||||
|
You need to add these secrets to your GitHub repository:
|
||||||
|
|
||||||
|
1. Go to **Settings** → **Secrets and variables** → **Actions**
|
||||||
|
2. Add these secrets:
|
||||||
|
- `DOCKER_USERNAME`: Your Docker Hub username
|
||||||
|
- `DOCKER_TOKEN`: Your Docker Hub access token
|
||||||
|
|
||||||
|
### **2. Get Docker Hub Token**
|
||||||
|
|
||||||
|
1. Go to [Docker Hub](https://hub.docker.com)
|
||||||
|
2. Account Settings → Security → New Access Token
|
||||||
|
3. Set permissions to "Read, Write, Delete"
|
||||||
|
4. Copy the token and add it to GitHub Secrets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 **Troubleshooting Common Issues**
|
||||||
|
|
||||||
|
### **❌ "Your local changes would be overwritten by merge"**
|
||||||
|
|
||||||
|
**Problem:** You have uncommitted changes
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Your commit message"
|
||||||
|
npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
### **❌ "Permission denied" in GitHub Actions**
|
||||||
|
|
||||||
|
**Problem:** Missing Docker Hub credentials
|
||||||
|
**Solution:** Add `DOCKER_USERNAME` and `DOCKER_TOKEN` to GitHub Secrets
|
||||||
|
|
||||||
|
### **❌ "Tag already exists"**
|
||||||
|
|
||||||
|
**Problem:** You've run the same release before
|
||||||
|
**Solution:** This is normal! The script will skip creating duplicate tags
|
||||||
|
|
||||||
|
### **❌ GitHub Actions fails**
|
||||||
|
|
||||||
|
**Problem:** Various build issues
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
1. Check Actions tab for detailed logs
|
||||||
|
2. Verify Docker Hub credentials
|
||||||
|
3. Check Dockerfile for syntax errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 **Testing Your Release System**
|
||||||
|
|
||||||
|
### **Quick Test:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make a small change
|
||||||
|
echo "// Test" >> src/js/main.js
|
||||||
|
git add src/js/main.js
|
||||||
|
git commit -m "Test release"
|
||||||
|
npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Verify Results:**
|
||||||
|
|
||||||
|
1. **GitHub Actions**: Check Actions tab for successful build
|
||||||
|
2. **Docker Hub**: Verify images are published
|
||||||
|
3. **Git Tags**: `git tag --list` should show new tag
|
||||||
|
4. **Version**: `cat package.json | grep version` should show updated version
|
||||||
|
|
||||||
|
### **Undo Test Release:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -d v1.0.1
|
||||||
|
git push origin :refs/tags/v1.0.1
|
||||||
|
git reset --hard HEAD~1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 **That's It!**
|
||||||
|
|
||||||
|
Your release system is now ready! Just follow the scenarios above based on your situation and run the appropriate `npm run release` command.
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
# Simple Mode for BentoPDF
|
||||||
|
|
||||||
|
Simple Mode is designed for internal organizational use where you want to hide all branding and marketing content, showing only the essential PDF tools for your users.
|
||||||
|
|
||||||
|
## What Simple Mode Does
|
||||||
|
|
||||||
|
When enabled, Simple Mode will:
|
||||||
|
|
||||||
|
- Hide the navigation bar
|
||||||
|
- Hide the hero section with marketing content
|
||||||
|
- Hide the features section
|
||||||
|
- Hide the security/compliance section
|
||||||
|
- Hide the FAQ section
|
||||||
|
- Hide the testimonials section
|
||||||
|
- Hide the support section
|
||||||
|
- Hide the footer
|
||||||
|
- Update the page title to "PDF Tools"
|
||||||
|
- Make the tools section more prominent
|
||||||
|
|
||||||
|
## How to Enable Simple Mode
|
||||||
|
|
||||||
|
### Method 1: Using Pre-built Simple Mode Image (Recommended)
|
||||||
|
|
||||||
|
Use the pre-built Simple Mode image directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 3000:80 bentopdf/bentopdf-simple:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with Docker Compose:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
image: bentopdf/bentopdf-simple:latest
|
||||||
|
container_name: bentopdf
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '3000:80'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
|
||||||
|
docker run -p 3000:80 bentopdf-simple
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SIMPLE_MODE=true
|
||||||
|
npm run build
|
||||||
|
npx serve dist -p 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing Simple Mode 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
|
||||||
|
# Build with simple mode
|
||||||
|
SIMPLE_MODE=true npm run build
|
||||||
|
|
||||||
|
# Serve the built files
|
||||||
|
npx serve dist -p 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
When Simple Mode is working correctly, you should see:
|
||||||
|
|
||||||
|
- ✅ Clean "PDF Tools" header (no marketing hero section)
|
||||||
|
- ✅ "Select a tool to get started" subtitle
|
||||||
|
- ✅ Search bar for tools
|
||||||
|
- ✅ All PDF tool cards organized by category
|
||||||
|
- ❌ No navigation bar
|
||||||
|
- ❌ No hero section with "The PDF Toolkit built for privacy"
|
||||||
|
- ❌ No features, FAQ, testimonials, or footer sections
|
||||||
|
|
||||||
|
## 📦 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
|
||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
image: bentopdf/bentopdf-simple:latest
|
||||||
|
container_name: bentopdf
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
environment:
|
||||||
|
- 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
|
||||||
+232
-80
@@ -1,25 +1,35 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<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>About Bentopdf - Fast, Private, and Free PDF Tools</title>
|
<title>About Bentopdf - Fast, Private, and Free PDF Tools</title>
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<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 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">
|
||||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bentopdf Logo" class="h-8 w-8 mr-2">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
id="home-logo"
|
||||||
</a>
|
>
|
||||||
|
<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 -->
|
||||||
<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>
|
||||||
@@ -27,56 +37,132 @@
|
|||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Mobile Hamburger Button -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="md:hidden flex items-center">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
|
|
||||||
<section id="about-hero" class="text-center py-16 md:py-24">
|
<section id="about-hero" class="text-center py-16 md:py-24">
|
||||||
<h1 class="text-3xl md:text-6xl font-bold text-white mb-4">
|
<h1 class="text-3xl md:text-6xl font-bold text-white mb-4">
|
||||||
We believe PDF tools should be <span class="marker-slanted">fast, private, and free.</span>
|
We believe PDF tools should be
|
||||||
|
<span class="marker-slanted">fast, private, and free.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg md:text-xl text-gray-400">
|
<p class="text-lg md:text-xl text-gray-400">No compromises.</p>
|
||||||
No compromises.
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
|
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<i data-lucide="rocket" class="w-16 h-16 text-indigo-400 mx-auto mb-6"></i>
|
<i
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Our Mission</h2>
|
data-lucide="rocket"
|
||||||
|
class="w-16 h-16 text-indigo-400 mx-auto mb-6"
|
||||||
|
></i>
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||||
|
Our Mission
|
||||||
|
</h2>
|
||||||
<p class="text-lg text-gray-400 leading-relaxed">
|
<p class="text-lg text-gray-400 leading-relaxed">
|
||||||
To provide the most comprehensive PDF toolbox that respects your privacy and never asks for payment.
|
To provide the most comprehensive PDF toolbox that respects your
|
||||||
We believe essential document tools should be accessible to everyone, everywhere, without barriers.
|
privacy and never asks for payment. We believe essential document
|
||||||
|
tools should be accessible to everyone, everywhere, without
|
||||||
|
barriers.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700">
|
<div
|
||||||
|
class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700"
|
||||||
|
>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||||
<div class="text-center md:text-left">
|
<div class="text-center md:text-left">
|
||||||
<span class="text-indigo-400 font-bold uppercase">Our Core Philosophy</span>
|
<span class="text-indigo-400 font-bold uppercase"
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">Privacy First. Always.</h2>
|
>Our Core Philosophy</span
|
||||||
|
>
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">
|
||||||
|
Privacy First. Always.
|
||||||
|
</h2>
|
||||||
<p class="text-gray-400 leading-relaxed">
|
<p class="text-gray-400 leading-relaxed">
|
||||||
In an era where data is a commodity, we take a different approach. All processing for Bentopdf
|
In an era where data is a commodity, we take a different approach.
|
||||||
tools happens locally in your browser. This means your files never touch our servers, we never
|
All processing for Bentopdf tools happens locally in your browser.
|
||||||
see your documents, and we don't track what you do. Your documents remain completely and
|
This means your files never touch our servers, we never see your
|
||||||
unequivocally private. It's not just a feature; it's our foundation.
|
documents, and we don't track what you do. Your documents remain
|
||||||
|
completely and unequivocally private. It's not just a feature;
|
||||||
|
it's our foundation.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="relative w-48 h-48">
|
<div class="relative w-48 h-48">
|
||||||
<div class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"></div>
|
<div
|
||||||
<div class="absolute inset-4 bg-indigo-500 rounded-full opacity-30 animate-pulse delay-500">
|
class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"
|
||||||
</div>
|
></div>
|
||||||
<i data-lucide="shield-check" class="w-48 h-48 text-indigo-400"></i>
|
<div
|
||||||
|
class="absolute inset-4 bg-indigo-500 rounded-full opacity-30 animate-pulse delay-500"
|
||||||
|
></div>
|
||||||
|
<i
|
||||||
|
data-lucide="shield-check"
|
||||||
|
class="w-48 h-48 text-indigo-400"
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,39 +174,60 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
|
||||||
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
||||||
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
<i
|
||||||
|
data-lucide="zap"
|
||||||
|
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
||||||
|
></i>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-white">Built for Speed</h3>
|
<h3 class="text-xl font-bold text-white">Built for Speed</h3>
|
||||||
<p class="text-gray-400 mt-2">No waiting for uploads or downloads to a server. By processing
|
<p class="text-gray-400 mt-2">
|
||||||
files directly in your browser using modern web technologies like WebAssembly, we offer
|
No waiting for uploads or downloads to a server. By processing
|
||||||
unparalleled speed for all our tools.</p>
|
files directly in your browser using modern web technologies
|
||||||
</div>
|
like WebAssembly, we offer unparalleled speed for all our tools.
|
||||||
</div>
|
|
||||||
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
|
||||||
<i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white">Completely Free</h3>
|
|
||||||
<p class="text-gray-400 mt-2">No trials, no subscriptions, no hidden fees, and no "premium"
|
|
||||||
features held hostage. We believe powerful PDF tools should be a public utility, not a
|
|
||||||
profit center.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
|
||||||
<i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white">No Account Required</h3>
|
|
||||||
<p class="text-gray-400 mt-2">Start using any tool immediately. We don't need your email, a
|
|
||||||
password, or any personal information. Your workflow should be frictionless and anonymous.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
||||||
<i data-lucide="code-2" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
<i
|
||||||
|
data-lucide="badge-dollar-sign"
|
||||||
|
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
||||||
|
></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white">Completely Free</h3>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
No trials, no subscriptions, no hidden fees, and no "premium"
|
||||||
|
features held hostage. We believe powerful PDF tools should be a
|
||||||
|
public utility, not a profit center.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
||||||
|
<i
|
||||||
|
data-lucide="user-plus"
|
||||||
|
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
||||||
|
></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white">No Account Required</h3>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
Start using any tool immediately. We don't need your email, a
|
||||||
|
password, or any personal information. Your workflow should be
|
||||||
|
frictionless and anonymous.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
||||||
|
<i
|
||||||
|
data-lucide="code-2"
|
||||||
|
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
||||||
|
></i>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-white">Open Source Spirit</h3>
|
<h3 class="text-xl font-bold text-white">Open Source Spirit</h3>
|
||||||
<p class="text-gray-400 mt-2">Built with transparency in mind. We leverage incredible
|
<p class="text-gray-400 mt-2">
|
||||||
open-source libraries like PDF-lib and PDF.js, and believe in the community-driven effort to
|
Built with transparency in mind. We leverage incredible
|
||||||
make powerful tools accessible to everyone.</p>
|
open-source libraries like PDF-lib and PDF.js, and believe in
|
||||||
|
the community-driven effort to make powerful tools accessible to
|
||||||
|
everyone.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,60 +236,105 @@
|
|||||||
<div class="section-divider"></div>
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
<section id="cta-section" class="text-center py-16">
|
<section id="cta-section" class="text-center py-16">
|
||||||
<h2 class="text-3xl font-bold text-white mb-4">Ready to get started?</h2>
|
<h2 class="text-3xl font-bold text-white mb-4">
|
||||||
|
Ready to get started?
|
||||||
|
</h2>
|
||||||
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||||
Join thousands of users who trust Bentopdf for their daily document needs. Experience the difference
|
Join thousands of users who trust Bentopdf for their daily document
|
||||||
that privacy and performance can make.
|
needs. Experience the difference that privacy and performance can
|
||||||
|
make.
|
||||||
</p>
|
</p>
|
||||||
<a href="index.html#tools-header"
|
<a
|
||||||
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">
|
href="index.html#tools-header"
|
||||||
|
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1"
|
||||||
|
>
|
||||||
Explore All Tools
|
Explore All Tools
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
|
>
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img src="images/favicon.svg" alt="Bentopdf Logo" class="h-10 w-10 mr-3">
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bentopdf Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
|
/>
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="./about.html" class="hover:text-indigo-400">About Us</a></li>
|
<li>
|
||||||
<li><a href="./faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
<a href="./about.html" class="hover:text-indigo-400"
|
||||||
<li><a href="./contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
>About Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
<li>
|
||||||
<li><a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
<a href="./terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="instagram"></i>
|
<i data-lucide="instagram"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.linkedin.com/company/bentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="linkedin"></i>
|
<i data-lucide="linkedin"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
href="https://x.com/BentoPDF"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,6 +346,6 @@
|
|||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+140
-32
@@ -1,23 +1,33 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<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>Contact Us - BentoPDF</title>
|
<title>Contact Us - BentoPDF</title>
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<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 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">
|
||||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl"> BentoPDF</span>
|
id="home-logo"
|
||||||
</a>
|
>
|
||||||
|
<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 -->
|
||||||
<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>
|
||||||
@@ -25,72 +35,170 @@
|
|||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Mobile Hamburger Button -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="md:hidden flex items-center">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
<section id="contact-hero" class="text-center py-16 md:py-24">
|
<section id="contact-hero" class="text-center py-16 md:py-24">
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">Get in Touch</h1>
|
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||||
|
Get in Touch
|
||||||
|
</h1>
|
||||||
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
||||||
We'd love to hear from you. Whether you have a question, feedback, or a feature request, please don't
|
We'd love to hear from you. Whether you have a question, feedback, or
|
||||||
hesitate to reach out.
|
a feature request, please don't hesitate to reach out.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="max-w-2xl mx-auto text-center py-8">
|
<div class="max-w-2xl mx-auto text-center py-8">
|
||||||
<p class="text-lg text-gray-400">
|
<p class="text-lg text-gray-400">
|
||||||
You can reach us directly by email at:
|
You can reach us directly by email at:
|
||||||
<a href="mailto:contact@bentopdf.com"
|
<a
|
||||||
class="text-indigo-400 underline hover:text-indigo-300">contact@bentopdf.com</a>
|
href="mailto:contact@bentopdf.com"
|
||||||
|
class="text-indigo-400 underline hover:text-indigo-300"
|
||||||
|
>contact@bentopdf.com</a
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
|
>
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3">
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
|
/>
|
||||||
<span class="text-xl font-bold text-white">Bentopdf</span>
|
<span class="text-xl font-bold text-white">Bentopdf</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">© 2025 Bentopdf. All rights reserved.</p>
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 Bentopdf. All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="about.html" class="hover:text-indigo-400">About Us</a></li>
|
<li>
|
||||||
|
<a href="about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||||
<li><a href="contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
<li>
|
||||||
|
<a href="contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
<li>
|
||||||
<li><a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
<a href="./terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="instagram"></i>
|
<i data-lucide="instagram"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.linkedin.com/company/bentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="linkedin"></i>
|
<i data-lucide="linkedin"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
href="https://x.com/BentoPDF"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,6 +211,6 @@
|
|||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: bentopdf
|
||||||
|
ports:
|
||||||
|
- '3000:80'
|
||||||
|
restart: unless-stopped
|
||||||
+5
-7
@@ -1,11 +1,9 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
bentopdf:
|
bentopdf:
|
||||||
build:
|
# simple mode - bentopdf/bentopdf-simple:latest
|
||||||
context: .
|
# default mode - bentopdf/bentopdf:latest
|
||||||
dockerfile: Dockerfile
|
image: bentopdf/bentopdf-simple:latest
|
||||||
container_name: bentopdf
|
container_name: bentopdf
|
||||||
ports:
|
|
||||||
- "3000:80"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '3000:80'
|
||||||
@@ -1,26 +1,36 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<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>Frequently Asked Questions - BentoPDF</title>
|
<title>Frequently Asked Questions - BentoPDF</title>
|
||||||
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<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 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">
|
||||||
<a href="index.html" 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 mr-2">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
id="home-logo"
|
||||||
</a>
|
>
|
||||||
|
<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 -->
|
||||||
<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>
|
||||||
@@ -28,116 +38,257 @@
|
|||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Mobile Hamburger Button -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="md:hidden flex items-center">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
|
|
||||||
<section id="faq-hero" class="text-center py-16 md:py-24">
|
<section id="faq-hero" class="text-center py-16 md:py-24">
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||||
Frequently Asked Questions
|
Frequently Asked Questions
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
||||||
Have questions? We've got answers. Here are some of the most common things people ask about BentoPDF.
|
Have questions? We've got answers. Here are some of the most common
|
||||||
|
things people ask about BentoPDF.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto space-y-4">
|
<div class="max-w-4xl mx-auto space-y-4">
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Are my files safe and private?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Are my files safe and private?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p><strong>Absolutely.</strong> This is the most important feature of BentoPDF. All processing
|
<p>
|
||||||
happens 100% locally in your web browser. Your files are never uploaded to any server, which
|
<strong>Absolutely.</strong> This is the most important feature of
|
||||||
means we—or anyone else—can never see them. Your privacy is guaranteed.</p>
|
BentoPDF. All processing happens 100% locally in your web browser.
|
||||||
|
Your files are never uploaded to any server, which means we—or
|
||||||
|
anyone else—can never see them. Your privacy is guaranteed.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Is BentoPDF really free? What's the catch?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Is BentoPDF really free? What's the catch?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>Yes, it's completely free, and there's no catch. There are no hidden fees, no subscription plans,
|
<p>
|
||||||
no usage limits, and no premium-only features. We believe essential tools should be accessible
|
Yes, it's completely free, and there's no catch. There are no
|
||||||
to everyone.</p>
|
hidden fees, no subscription plans, no usage limits, and no
|
||||||
|
premium-only features. We believe essential tools should be
|
||||||
|
accessible to everyone.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Do I need an internet connection to use the tools?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Do I need an internet connection to use the tools?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>After you load the website for the first time, most tools will work completely offline! Because
|
<p>
|
||||||
all the processing libraries are loaded into your browser, you can disconnect from the internet
|
After you load the website for the first time, most tools will
|
||||||
and continue to merge, split, compress, and edit your PDFs securely. Some niche tools that
|
work completely offline! Because all the processing libraries are
|
||||||
require external data (like Markdown to PDF with web images) may need a connection.</p>
|
loaded into your browser, you can disconnect from the internet and
|
||||||
|
continue to merge, split, compress, and edit your PDFs securely.
|
||||||
|
Some niche tools that require external data (like Markdown to PDF
|
||||||
|
with web images) may need a connection.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Are there any file size or usage limitations?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Are there any file size or usage limitations?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>No, we do not impose any artificial limits on file size, the number of files, or how many times
|
<p>
|
||||||
you can use a tool. The only practical limitation is the processing power and memory of your own
|
No, we do not impose any artificial limits on file size, the
|
||||||
computer, as very large or complex files may take longer to process.</p>
|
number of files, or how many times you can use a tool. The only
|
||||||
|
practical limitation is the processing power and memory of your
|
||||||
|
own computer, as very large or complex files may take longer to
|
||||||
|
process.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Why did my PDF fail to process?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Why did my PDF fail to process?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>Failures are rare but can happen for a few reasons:</p>
|
<p>Failures are rare but can happen for a few reasons:</p>
|
||||||
<ul class="list-disc list-inside mt-2 space-y-1">
|
<ul class="list-disc list-inside mt-2 space-y-1">
|
||||||
<li>The PDF might be corrupted or not compliant with standard specifications.</li>
|
<li>
|
||||||
<li>The file could be encrypted with a password you don't have. Please use our Decrypt tool
|
The PDF might be corrupted or not compliant with standard
|
||||||
first.</li>
|
specifications.
|
||||||
<li>The file might be a special "XFA" or dynamic form-based PDF, which some of our tools cannot
|
</li>
|
||||||
process.</li>
|
<li>
|
||||||
|
The file could be encrypted with a password you don't have.
|
||||||
|
Please use our Decrypt tool first.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The file might be a special "XFA" or dynamic form-based PDF,
|
||||||
|
which some of our tools cannot process.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Do you track my activity on BentoPDF?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Do you track my activity on BentoPDF?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>We care about your privacy. BentoPDF does not track personal information. We use <a
|
<p>
|
||||||
href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank"
|
We care about your privacy. BentoPDF does not track personal
|
||||||
rel="noopener noreferrer">Simple Analytics</a> solely to see anonymous visit counts. This
|
information. We use
|
||||||
means we can know how many users visit our site, but <strong>we never know who you are</strong>.
|
<a
|
||||||
Simple Analytics is fully GDPR-compliant and respects your privacy.</p>
|
href="https://simpleanalytics.com"
|
||||||
|
class="text-indigo-400 hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Simple Analytics</a
|
||||||
|
>
|
||||||
|
solely to see anonymous visit counts. This means we can know how
|
||||||
|
many users visit our site, but
|
||||||
|
<strong>we never know who you are</strong>. Simple Analytics is
|
||||||
|
fully GDPR-compliant and respects your privacy.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">What technology does BentoPDF use?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
What technology does BentoPDF use?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>BentoPDF is built on the power of modern web technologies. We primarily use JavaScript libraries
|
<p>
|
||||||
like <strong>PDF-lib.js</strong> and <strong>PDFKit.js</strong> that are compiled to run
|
BentoPDF is built on the power of modern web technologies. We
|
||||||
efficiently in your browser via WebAssembly. This allows for powerful, server-like processing
|
primarily use JavaScript libraries like
|
||||||
without your data ever leaving your machine.</p>
|
<strong>PDF-lib.js</strong> and <strong>PDFKit.js</strong> that
|
||||||
|
are compiled to run efficiently in your browser via WebAssembly.
|
||||||
|
This allows for powerful, server-like processing without your data
|
||||||
|
ever leaving your machine.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,56 +296,98 @@
|
|||||||
<div class="section-divider mt-16"></div>
|
<div class="section-divider mt-16"></div>
|
||||||
|
|
||||||
<section id="contact-section" class="text-center py-16">
|
<section id="contact-section" class="text-center py-16">
|
||||||
<h2 class="text-3xl font-bold text-white mb-4">Still have questions?</h2>
|
<h2 class="text-3xl font-bold text-white mb-4">
|
||||||
|
Still have questions?
|
||||||
|
</h2>
|
||||||
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||||
If you can't find the answer you're looking for, feel free to reach out to our support team. We're
|
If you can't find the answer you're looking for, feel free to reach
|
||||||
always Bento to help.
|
out to our support team. We're always Bento to help.
|
||||||
</p>
|
</p>
|
||||||
<a href="contact.html"
|
<a
|
||||||
class="inline-block px-8 py-3 rounded-full bg-gray-700 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:bg-gray-600 transition-colors duration-200">
|
href="contact.html"
|
||||||
|
class="inline-block px-8 py-3 rounded-full bg-gray-700 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:bg-gray-600 transition-colors duration-200"
|
||||||
|
>
|
||||||
Contact Us
|
Contact Us
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
|
>
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3">
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
|
/>
|
||||||
<span class="text-xl font-bold text-white">Bento PDF</span>
|
<span class="text-xl font-bold text-white">Bento PDF</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">© 2025 Bento PDF. All rights reserved.</p>
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 Bento PDF. All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="about.html" class="hover:text-indigo-400">About Us</a></li>
|
<li>
|
||||||
|
<a href="about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||||
<li><a href="contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
<li>
|
||||||
|
<a href="contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
<li>
|
||||||
<li><a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
<a href="./terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"><i data-lucide="instagram"></i></a>
|
<a
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"><i data-lucide="linkedin"></i></a>
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
><i data-lucide="instagram"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/company/bentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
><i data-lucide="linkedin"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
href="https://x.com/BentoPDF"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,6 +399,6 @@
|
|||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+571
-240
File diff suppressed because it is too large.
Load diff
Generated
+312
-221
@@ -1,15 +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": {
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@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",
|
||||||
|
"@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",
|
||||||
@@ -17,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",
|
||||||
@@ -31,16 +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",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
@@ -84,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": {
|
||||||
@@ -2574,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",
|
||||||
@@ -2701,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",
|
||||||
@@ -2990,6 +3050,12 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@neslinesli93/qpdf-wasm": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@neslinesli93/qpdf-wasm/-/qpdf-wasm-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-ObyTmabopTMwN6AGwrEkmDbdmM5wnwlRdn9jnJYQ2KbBUPdPDcfNf1QkcWFOVD+qIgSqILz9nK51e22T+x+gkQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -3412,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"
|
||||||
],
|
],
|
||||||
@@ -3471,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"
|
||||||
],
|
],
|
||||||
@@ -3487,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"
|
||||||
],
|
],
|
||||||
@@ -3503,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"
|
||||||
],
|
],
|
||||||
@@ -3519,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"
|
||||||
],
|
],
|
||||||
@@ -3535,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"
|
||||||
],
|
],
|
||||||
@@ -3551,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"
|
||||||
],
|
],
|
||||||
@@ -3567,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"
|
||||||
],
|
],
|
||||||
@@ -3583,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"
|
||||||
],
|
],
|
||||||
@@ -3599,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",
|
||||||
@@ -3619,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"
|
||||||
},
|
},
|
||||||
@@ -3627,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"
|
||||||
],
|
],
|
||||||
@@ -3644,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"
|
||||||
],
|
],
|
||||||
@@ -3660,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"
|
||||||
@@ -3793,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": {
|
||||||
@@ -3851,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",
|
||||||
@@ -5038,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",
|
||||||
@@ -8080,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",
|
||||||
@@ -8103,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": {
|
||||||
@@ -8298,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"
|
||||||
@@ -8313,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"
|
||||||
],
|
],
|
||||||
@@ -8346,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"
|
||||||
],
|
],
|
||||||
@@ -8366,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"
|
||||||
],
|
],
|
||||||
@@ -8386,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"
|
||||||
],
|
],
|
||||||
@@ -8406,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"
|
||||||
],
|
],
|
||||||
@@ -8426,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"
|
||||||
],
|
],
|
||||||
@@ -8446,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"
|
||||||
],
|
],
|
||||||
@@ -8466,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"
|
||||||
],
|
],
|
||||||
@@ -8486,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"
|
||||||
],
|
],
|
||||||
@@ -8506,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"
|
||||||
],
|
],
|
||||||
@@ -8622,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": {
|
||||||
@@ -8847,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",
|
||||||
@@ -9462,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": {
|
||||||
@@ -9763,6 +9863,22 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-format": {
|
"node_modules/pretty-format": {
|
||||||
"version": "27.5.1",
|
"version": "27.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
@@ -11239,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": {
|
||||||
@@ -11257,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",
|
||||||
@@ -12232,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",
|
||||||
@@ -12747,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",
|
||||||
|
|||||||
+21
-7
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.1.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -14,26 +14,40 @@
|
|||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:watch": "vitest watch",
|
"test:watch": "vitest watch",
|
||||||
"build:docker": "vite build"
|
"build:docker": "vite build",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"release": "node scripts/release.js patch",
|
||||||
|
"release:minor": "node scripts/release.js minor",
|
||||||
|
"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",
|
||||||
"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": {
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@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",
|
||||||
|
"@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",
|
||||||
@@ -41,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",
|
||||||
|
|||||||
+234
-74
@@ -1,25 +1,35 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<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>Privacy Policy - BentoPDF</title>
|
<title>Privacy Policy - BentoPDF</title>
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<script src="https://unpkg.com/lucide@latest"></script>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<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 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">
|
||||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
|
<div
|
||||||
<img src="images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8 mr-2">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
id="home-logo"
|
||||||
</a>
|
>
|
||||||
|
<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 -->
|
||||||
<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>
|
||||||
@@ -27,130 +37,280 @@
|
|||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Mobile Hamburger Button -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="md:hidden flex items-center">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="container mx-auto p-4 md:p-8">
|
<div id="app" class="container mx-auto p-4 md:p-8">
|
||||||
<section class="max-w-4xl mx-auto py-12">
|
<section class="max-w-4xl mx-auto py-12">
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-center text-white mb-4">Privacy Policy</h1>
|
<h1 class="text-4xl md:text-5xl font-bold text-center text-white mb-4">
|
||||||
<p class="text-center text-gray-500">Last Updated: September 14, 2025</p>
|
Privacy Policy
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-500">
|
||||||
|
Last Updated: September 14, 2025
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="legal-content mt-12">
|
<div class="legal-content mt-12">
|
||||||
<h2>1. Our Commitment to Privacy</h2>
|
<h2>1. Our Commitment to Privacy</h2>
|
||||||
<p>BentoPDF ("we", "us", "our") is fundamentally a privacy-focused service. This Privacy Policy outlines
|
<p>
|
||||||
our unwavering commitment to protecting your privacy. Our core principle is simple: <strong>your
|
BentoPDF ("we", "us", "our") is fundamentally a privacy-focused
|
||||||
files are your files</strong>. We do not and cannot view, access, store, or share your
|
service. This Privacy Policy outlines our unwavering commitment to
|
||||||
documents. All PDF processing occurs entirely on your own computer, within your web browser
|
protecting your privacy. Our core principle is simple:
|
||||||
(client-side).</p>
|
<strong>your files are your files</strong>. We do not and cannot
|
||||||
|
view, access, store, or share your documents. All PDF processing
|
||||||
|
occurs entirely on your own computer, within your web browser
|
||||||
|
(client-side).
|
||||||
|
</p>
|
||||||
|
|
||||||
<h3>1.1 The Client-Side Principle</h3>
|
<h3>1.1 The Client-Side Principle</h3>
|
||||||
<p>Unlike other online PDF services, BentoPDF does not upload your files to a server for processing. The
|
<p>
|
||||||
tools you use are powered by JavaScript and WebAssembly libraries that run directly on your device.
|
Unlike other online PDF services, BentoPDF does not upload your
|
||||||
This means your data never leaves your computer, providing you with the highest level of privacy and
|
files to a server for processing. The tools you use are powered by
|
||||||
security.</p>
|
JavaScript and WebAssembly libraries that run directly on your
|
||||||
|
device. This means your data never leaves your computer, providing
|
||||||
|
you with the highest level of privacy and security.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>2. Information We Do Not Collect</h2>
|
<h2>2. Information We Do Not Collect</h2>
|
||||||
<p>Because of our client-side architecture, we are technically incapable of collecting the following
|
<p>
|
||||||
information:</p>
|
Because of our client-side architecture, we are technically
|
||||||
|
incapable of collecting the following information:
|
||||||
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>The content of your PDF files or any other documents you use with our tools.</li>
|
<li>
|
||||||
|
The content of your PDF files or any other documents you use with
|
||||||
|
our tools.
|
||||||
|
</li>
|
||||||
<li>Any personal data contained within your documents.</li>
|
<li>Any personal data contained within your documents.</li>
|
||||||
<li>Filenames of your documents.</li>
|
<li>Filenames of your documents.</li>
|
||||||
<li>Any derived information or metadata from your files, beyond what is necessary for the tool to
|
<li>
|
||||||
function during your active session (and this is immediately discarded).</li>
|
Any derived information or metadata from your files, beyond what
|
||||||
|
is necessary for the tool to function during your active session
|
||||||
|
(and this is immediately discarded).
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>3. Information We May Collect (Non-Personal Data)</h2>
|
<h2>3. Information We May Collect (Non-Personal Data)</h2>
|
||||||
<p>To improve our website and services, we may collect anonymous, non-personally identifiable
|
<p>
|
||||||
information. This type of data helps us understand how users interact with our site, which tools are
|
To improve our website and services, we may collect anonymous,
|
||||||
most popular, and how we can improve the user experience. This includes:</p>
|
non-personally identifiable information. This type of data helps us
|
||||||
|
understand how users interact with our site, which tools are most
|
||||||
|
popular, and how we can improve the user experience. This includes:
|
||||||
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Usage Analytics:</strong> Anonymized data such as which tools are used, how often they
|
<li>
|
||||||
are used, and which features are accessed. This is aggregated and cannot be tied back to an
|
<strong>Usage Analytics:</strong> Anonymized data such as which
|
||||||
individual user or document.</li>
|
tools are used, how often they are used, and which features are
|
||||||
<li><strong>Performance Data:</strong> Anonymized error reports or performance metrics to help us
|
accessed. This is aggregated and cannot be tied back to an
|
||||||
identify and fix bugs. This data contains no personal information or file content.</li>
|
individual user or document.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Performance Data:</strong> Anonymized error reports or
|
||||||
|
performance metrics to help us identify and fix bugs. This data
|
||||||
|
contains no personal information or file content.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>We use privacy-respecting analytics platforms for this purpose. Specifically, we use
|
<p>
|
||||||
<a href="https://simpleanalytics.com" target="_blank" rel="noopener noreferrer"
|
We use privacy-respecting analytics platforms for this purpose.
|
||||||
class="text-indigo-400 hover:underline">Simple Analytics</a>
|
Specifically, we use
|
||||||
to track anonymous visit counts. This means we can see how many users visit our site, but
|
<a
|
||||||
<strong>we never collect personal information or identify individual users</strong>.
|
href="https://simpleanalytics.com"
|
||||||
Simple Analytics is fully GDPR-compliant and respects user privacy. We do not use tracking cookies
|
target="_blank"
|
||||||
for advertising or cross-site profiling.
|
rel="noopener noreferrer"
|
||||||
|
class="text-indigo-400 hover:underline"
|
||||||
|
>Simple Analytics</a
|
||||||
|
>
|
||||||
|
to track anonymous visit counts. This means we can see how many
|
||||||
|
users visit our site, but
|
||||||
|
<strong
|
||||||
|
>we never collect personal information or identify individual
|
||||||
|
users</strong
|
||||||
|
>. Simple Analytics is fully GDPR-compliant and respects user
|
||||||
|
privacy. We do not use tracking cookies for advertising or
|
||||||
|
cross-site profiling.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>4. Third-Party Libraries</h2>
|
<h2>4. Third-Party Libraries</h2>
|
||||||
<p>BentoPDF is built using powerful, open-source libraries like PDF-lib.js and PDF.js. These libraries
|
<p>
|
||||||
are trusted by developers worldwide and operate under the same client-side principle. While we have
|
BentoPDF is built using powerful, open-source libraries like
|
||||||
vetted these libraries, we encourage you to review their respective privacy policies for your own
|
PDF-lib.js and PDF.js. These libraries are trusted by developers
|
||||||
peace of mind.</p>
|
worldwide and operate under the same client-side principle. While we
|
||||||
|
have vetted these libraries, we encourage you to review their
|
||||||
|
respective privacy policies for your own peace of mind.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>5. Security</h2>
|
<h2>5. Security</h2>
|
||||||
<p>Since your files are never transmitted over the internet to our servers, you are protected from
|
<p>
|
||||||
potential data breaches during transit or storage on a server. The security of your documents is in
|
Since your files are never transmitted over the internet to our
|
||||||
your hands and protected by the security of your own computer and web browser.</p>
|
servers, you are protected from potential data breaches during
|
||||||
|
transit or storage on a server. The security of your documents is in
|
||||||
|
your hands and protected by the security of your own computer and
|
||||||
|
web browser.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>6. Children's Privacy</h2>
|
<h2>6. Children's Privacy</h2>
|
||||||
<p>Our services are not directed at individuals under the age of 13. We do not knowingly collect any
|
<p>
|
||||||
personal information from children. If you believe a child has provided us with personal
|
Our services are not directed at individuals under the age of 13. We
|
||||||
information, please contact us, and we will take steps to delete such information.</p>
|
do not knowingly collect any personal information from children. If
|
||||||
|
you believe a child has provided us with personal information,
|
||||||
|
please contact us, and we will take steps to delete such
|
||||||
|
information.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>7. Changes to This Privacy Policy</h2>
|
<h2>7. Changes to This Privacy Policy</h2>
|
||||||
<p>We may update this Privacy Policy from time to time. We will notify you of any changes by posting the
|
<p>
|
||||||
new policy on this page and updating the "Last Updated" date at the top. You are advised to review
|
We may update this Privacy Policy from time to time. We will notify
|
||||||
this Privacy Policy periodically for any changes.</p>
|
you of any changes by posting the new policy on this page and
|
||||||
|
updating the "Last Updated" date at the top. You are advised to
|
||||||
|
review this Privacy Policy periodically for any changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>8. Contact Us</h2>
|
<h2>8. Contact Us</h2>
|
||||||
<p>If you have any questions about this Privacy Policy, please contact us at <a
|
<p>
|
||||||
href="mailto:contact@bentopdf.com">contact@bentopdf.com</a>.</p>
|
If you have any questions about this Privacy Policy, please contact
|
||||||
|
us at
|
||||||
|
<a href="mailto:contact@bentopdf.com">contact@bentopdf.com</a>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
|
>
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img src="images/favicon.svg" alt="BentoPDF Logo" class="h-10 w-10 mr-3">
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="BentoPDF Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
|
/>
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="about.html" class="hover:text-indigo-400">About Us</a></li>
|
<li>
|
||||||
|
<a href="about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||||
<li><a href="contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
<li>
|
||||||
|
<a href="contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
<li>
|
||||||
<li><a href="privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
<a href="terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="instagram"></i>
|
<i data-lucide="instagram"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.linkedin.com/company/bentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="linkedin"></i>
|
<i data-lucide="linkedin"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
href="https://x.com/BentoPDF"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,6 +321,6 @@
|
|||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -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)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
+1
-1
@@ -37,7 +37,7 @@ function processFile(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function processDirectory(dirPath) {
|
function processDirectory(dirPath) {
|
||||||
fs.readdirSync(dirPath).forEach(file => {
|
fs.readdirSync(dirPath).forEach((file) => {
|
||||||
const filePath = path.join(dirPath, file);
|
const filePath = path.join(dirPath, file);
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const packageJsonPath = path.join(__dirname, '../package.json');
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
|
||||||
|
function getCurrentVersion() {
|
||||||
|
return packageJson.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVersion(type) {
|
||||||
|
const currentVersion = getCurrentVersion();
|
||||||
|
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||||
|
|
||||||
|
let newVersion;
|
||||||
|
switch (type) {
|
||||||
|
case 'major':
|
||||||
|
newVersion = `${major + 1}.0.0`;
|
||||||
|
break;
|
||||||
|
case 'minor':
|
||||||
|
newVersion = `${major}.${minor + 1}.0`;
|
||||||
|
break;
|
||||||
|
case 'patch':
|
||||||
|
default:
|
||||||
|
newVersion = `${major}.${minor}.${patch + 1}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
packageJson.version = newVersion;
|
||||||
|
fs.writeFileSync(
|
||||||
|
packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2) + '\n'
|
||||||
|
);
|
||||||
|
return newVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGitTag(version) {
|
||||||
|
const tagName = `v${version}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if tag already exists
|
||||||
|
execSync(`git rev-parse "v${version}" >/dev/null 2>&1`, {
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
console.log(`✅ Tag v${version} already exists`);
|
||||||
|
return tagName;
|
||||||
|
} catch {
|
||||||
|
// Tag doesn't exist, create it
|
||||||
|
execSync(`git tag -a "v${version}" -m "Release v${version}"`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
console.log(`✅ Created tag v${version}`);
|
||||||
|
return tagName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const type = process.argv[2] || 'patch';
|
||||||
|
|
||||||
|
if (!['major', 'minor', 'patch'].includes(type)) {
|
||||||
|
console.error('❌ Invalid version type. Use: major, minor, or patch');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🚀 Releasing ${type} version...`);
|
||||||
|
|
||||||
|
// 1. Update version in package.json
|
||||||
|
const newVersion = updateVersion(type);
|
||||||
|
console.log(`📦 Updated version to ${newVersion}`);
|
||||||
|
|
||||||
|
// 2. Add and commit changes
|
||||||
|
execSync('git add package.json', { stdio: 'inherit' });
|
||||||
|
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`💾 Committed version change`);
|
||||||
|
|
||||||
|
// 3. Create git tag
|
||||||
|
const tagName = createGitTag(newVersion);
|
||||||
|
|
||||||
|
// 4. Push everything to main
|
||||||
|
console.log(`📤 Pushing to main...`);
|
||||||
|
execSync('git push origin main', { stdio: 'inherit' });
|
||||||
|
execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
console.log(`🎉 Release v${newVersion} complete!`);
|
||||||
|
console.log(`📦 Docker image: bentopdf/bentopdf:${newVersion}`);
|
||||||
|
console.log(
|
||||||
|
`🏷️ GitHub release: https://github.com/AltuisticIsopod/bentopdf/releases/tag/${tagName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
+63
-9
@@ -1,4 +1,48 @@
|
|||||||
@import "tailwindcss";
|
@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';
|
||||||
|
|
||||||
|
@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;
|
||||||
@@ -11,18 +55,24 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-card {
|
.tool-card {
|
||||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
transition:
|
||||||
|
transform 0.2s ease-in-out,
|
||||||
|
box-shadow 0.2s ease-in-out;
|
||||||
border: 1px solid #374151;
|
border: 1px solid #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-card:hover {
|
.tool-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.2),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||||
border-color: #4f46e5;
|
border-color: #4f46e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
|
transition:
|
||||||
|
background-color 0.2s ease-in-out,
|
||||||
|
transform 0.1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:active {
|
.btn:active {
|
||||||
@@ -30,7 +80,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Custom file input */
|
/* Custom file input */
|
||||||
input[type="file"]::file-selector-button {
|
input[type='file']::file-selector-button {
|
||||||
@apply bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors duration-200 mr-4;
|
@apply bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors duration-200 mr-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +115,7 @@ input[type="file"]::file-selector-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#tool-interface {
|
#tool-interface {
|
||||||
color: #39A0ED;
|
color: #39a0ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-thumbnail,
|
.page-thumbnail,
|
||||||
@@ -197,7 +247,7 @@ footer a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.marker-slanted::before {
|
.marker-slanted::before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -236,7 +286,9 @@ footer a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-gradient:hover {
|
.btn-gradient:hover {
|
||||||
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3), 0 4px 6px -4px rgba(79, 70, 229, 0.3);
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(79, 70, 229, 0.3),
|
||||||
|
0 4px 6px -4px rgba(79, 70, 229, 0.3);
|
||||||
/* hover:shadow-xl hover:shadow-indigo-500/30 */
|
/* hover:shadow-xl hover:shadow-indigo-500/30 */
|
||||||
transform: translateY(-0.25rem);
|
transform: translateY(-0.25rem);
|
||||||
/* hover:-translate-y-1 */
|
/* hover:-translate-y-1 */
|
||||||
@@ -245,7 +297,9 @@ footer a {
|
|||||||
.btn-gradient:focus {
|
.btn-gradient:focus {
|
||||||
outline: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
box-shadow: 0 0 0 2px #111827, 0 0 0 4px #818cf8;
|
box-shadow:
|
||||||
|
0 0 0 2px #111827,
|
||||||
|
0 0 0 4px #818cf8;
|
||||||
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
|
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-18
@@ -1,7 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from './ui.js';
|
import { showLoader, hideLoader, showAlert } from './ui.js';
|
||||||
import { state } from './state.js';
|
import { state } from './state.js';
|
||||||
import { toolLogic } from './logic/index.js';
|
import { toolLogic } from './logic/index.js';
|
||||||
import { icons, createIcons } from "lucide";
|
import { icons, createIcons } from 'lucide';
|
||||||
|
|
||||||
const editorState = {
|
const editorState = {
|
||||||
pdf: null,
|
pdf: null,
|
||||||
@@ -52,16 +52,20 @@ async function renderPage(num: any) {
|
|||||||
|
|
||||||
const renderContext = {
|
const renderContext = {
|
||||||
canvasContext: editorState.context,
|
canvasContext: editorState.context,
|
||||||
viewport: viewport
|
viewport: viewport,
|
||||||
};
|
};
|
||||||
|
|
||||||
await page.render(renderContext).promise;
|
await page.render(renderContext).promise;
|
||||||
|
|
||||||
editorState.pageSnapshot = editorState.context.getImageData(0, 0, editorState.canvas.width, editorState.canvas.height);
|
editorState.pageSnapshot = editorState.context.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
editorState.canvas.width,
|
||||||
|
editorState.canvas.height
|
||||||
|
);
|
||||||
redrawShapes();
|
redrawShapes();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error rendering page:", error);
|
console.error('Error rendering page:', error);
|
||||||
showAlert('Render Error', 'Could not display the page.');
|
showAlert('Render Error', 'Could not display the page.');
|
||||||
} finally {
|
} finally {
|
||||||
editorState.pageRendering = false;
|
editorState.pageRendering = false;
|
||||||
@@ -71,7 +75,8 @@ async function renderPage(num: any) {
|
|||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('prev-page').disabled = num <= 1;
|
document.getElementById('prev-page').disabled = num <= 1;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('next-page').disabled = num >= editorState.pdf.numPages;
|
document.getElementById('next-page').disabled =
|
||||||
|
num >= editorState.pdf.numPages;
|
||||||
|
|
||||||
if (editorState.pageNumPending !== null) {
|
if (editorState.pageNumPending !== null) {
|
||||||
const pendingPage = editorState.pageNumPending;
|
const pendingPage = editorState.pageNumPending;
|
||||||
@@ -100,7 +105,12 @@ function redrawShapes() {
|
|||||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||||
editorState.context.lineWidth = 2;
|
editorState.context.lineWidth = 2;
|
||||||
editorState.context.setLineDash([8, 4]);
|
editorState.context.setLineDash([8, 4]);
|
||||||
editorState.context.strokeRect(currentCropBox.x, currentCropBox.y, currentCropBox.width, currentCropBox.height);
|
editorState.context.strokeRect(
|
||||||
|
currentCropBox.x,
|
||||||
|
currentCropBox.y,
|
||||||
|
currentCropBox.width,
|
||||||
|
currentCropBox.height
|
||||||
|
);
|
||||||
editorState.context.setLineDash([]);
|
editorState.context.setLineDash([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +170,7 @@ function handleInteractionEnd() {
|
|||||||
|
|
||||||
editorState.cropBoxes[editorState.currentPageNum - 1] = {
|
editorState.cropBoxes[editorState.currentPageNum - 1] = {
|
||||||
...finalRect,
|
...finalRect,
|
||||||
scale: editorState.scale
|
scale: editorState.scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
editorState.lastInteractionRect = null; // Reset for the next drawing action
|
editorState.lastInteractionRect = null; // Reset for the next drawing action
|
||||||
@@ -187,7 +197,8 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
|
|
||||||
const prevButton = document.createElement('button');
|
const prevButton = document.createElement('button');
|
||||||
prevButton.id = 'prev-page';
|
prevButton.id = 'prev-page';
|
||||||
prevButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
prevButton.className =
|
||||||
|
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||||
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
|
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
|
||||||
|
|
||||||
const pageInfo = document.createElement('span');
|
const pageInfo = document.createElement('span');
|
||||||
@@ -197,11 +208,16 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
currentPageDisplay.id = 'current-page-display';
|
currentPageDisplay.id = 'current-page-display';
|
||||||
currentPageDisplay.textContent = '1';
|
currentPageDisplay.textContent = '1';
|
||||||
|
|
||||||
pageInfo.append('Page ', currentPageDisplay, ` of ${editorState.pdf.numPages}`);
|
pageInfo.append(
|
||||||
|
'Page ',
|
||||||
|
currentPageDisplay,
|
||||||
|
` of ${editorState.pdf.numPages}`
|
||||||
|
);
|
||||||
|
|
||||||
const nextButton = document.createElement('button');
|
const nextButton = document.createElement('button');
|
||||||
nextButton.id = 'next-page';
|
nextButton.id = 'next-page';
|
||||||
nextButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
nextButton.className =
|
||||||
|
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||||
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
||||||
|
|
||||||
pageNav.append(prevButton, pageInfo, nextButton);
|
pageNav.append(prevButton, pageInfo, nextButton);
|
||||||
@@ -209,10 +225,12 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
document.getElementById('prev-page').addEventListener('click', () => {
|
document.getElementById('prev-page').addEventListener('click', () => {
|
||||||
if (editorState.currentPageNum > 1) queueRenderPage(editorState.currentPageNum - 1);
|
if (editorState.currentPageNum > 1)
|
||||||
|
queueRenderPage(editorState.currentPageNum - 1);
|
||||||
});
|
});
|
||||||
document.getElementById('next-page').addEventListener('click', () => {
|
document.getElementById('next-page').addEventListener('click', () => {
|
||||||
if (editorState.currentPageNum < editorState.pdf.numPages) queueRenderPage(editorState.currentPageNum + 1);
|
if (editorState.currentPageNum < editorState.pdf.numPages)
|
||||||
|
queueRenderPage(editorState.currentPageNum + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// To prevent stacking multiple listeners, we replace the canvas element with a clone
|
// To prevent stacking multiple listeners, we replace the canvas element with a clone
|
||||||
@@ -228,8 +246,12 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
|
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
|
||||||
|
|
||||||
// Touch Events
|
// Touch Events
|
||||||
editorState.canvas.addEventListener('touchstart', handleInteractionStart, { passive: false });
|
editorState.canvas.addEventListener('touchstart', handleInteractionStart, {
|
||||||
editorState.canvas.addEventListener('touchmove', handleInteractionMove, { passive: false });
|
passive: false,
|
||||||
|
});
|
||||||
|
editorState.canvas.addEventListener('touchmove', handleInteractionMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
||||||
|
|
||||||
if (toolId === 'crop') {
|
if (toolId === 'crop') {
|
||||||
@@ -259,12 +281,20 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
|
|
||||||
document.getElementById('process-btn').onclick = async () => {
|
document.getElementById('process-btn').onclick = async () => {
|
||||||
if (Object.keys(editorState.cropBoxes).length === 0) {
|
if (Object.keys(editorState.cropBoxes).length === 0) {
|
||||||
showAlert('No Area Selected', 'Please draw a rectangle on at least one page to select the crop area.');
|
showAlert(
|
||||||
|
'No Area Selected',
|
||||||
|
'Please draw a rectangle on at least one page to select the crop area.'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const success = await toolLogic['crop-pdf'].process(editorState.cropBoxes);
|
const success = await toolLogic['crop-pdf'].process(
|
||||||
|
editorState.cropBoxes
|
||||||
|
);
|
||||||
if (success) {
|
if (success) {
|
||||||
showAlert('Success!', 'Your PDF has been cropped and the download has started.');
|
showAlert(
|
||||||
|
'Success!',
|
||||||
|
'Your PDF has been cropped and the download has started.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,67 @@
|
|||||||
export const singlePdfLoadTools = [
|
export const singlePdfLoadTools = [
|
||||||
'split', 'organize', 'rotate', 'add-page-numbers',
|
'split',
|
||||||
'pdf-to-jpg', 'pdf-to-png', 'pdf-to-webp', 'compress', 'pdf-to-greyscale',
|
'organize',
|
||||||
'edit-metadata', 'remove-metadata', 'flatten', 'delete-pages', 'add-blank-page',
|
'rotate',
|
||||||
'extract-pages', 'add-watermark', 'add-header-footer', 'invert-colors', 'view-metadata',
|
'add-page-numbers',
|
||||||
'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', 'pdf-to-tiff', 'split-in-half',
|
'pdf-to-jpg',
|
||||||
'page-dimensions', 'n-up', 'duplicate-organize', 'combine-single-page', 'fix-dimensions', 'change-background-color',
|
'pdf-to-png',
|
||||||
'change-text-color', 'ocr-pdf', 'sign-pdf', 'remove-annotations', 'cropper', 'form-filler', 'posterize'
|
'pdf-to-webp',
|
||||||
|
'compress',
|
||||||
|
'pdf-to-greyscale',
|
||||||
|
'edit-metadata',
|
||||||
|
'remove-metadata',
|
||||||
|
'flatten',
|
||||||
|
'delete-pages',
|
||||||
|
'add-blank-page',
|
||||||
|
'extract-pages',
|
||||||
|
'add-watermark',
|
||||||
|
'add-header-footer',
|
||||||
|
'invert-colors',
|
||||||
|
'view-metadata',
|
||||||
|
'crop',
|
||||||
|
'redact',
|
||||||
|
'pdf-to-bmp',
|
||||||
|
'pdf-to-tiff',
|
||||||
|
'split-in-half',
|
||||||
|
'page-dimensions',
|
||||||
|
'n-up',
|
||||||
|
'duplicate-organize',
|
||||||
|
'combine-single-page',
|
||||||
|
'fix-dimensions',
|
||||||
|
'change-background-color',
|
||||||
|
'change-text-color',
|
||||||
|
'ocr-pdf',
|
||||||
|
'sign-pdf',
|
||||||
|
'remove-annotations',
|
||||||
|
'cropper',
|
||||||
|
'form-filler',
|
||||||
|
'posterize',
|
||||||
|
'remove-blank-pages',
|
||||||
|
'add-attachments',
|
||||||
|
'sanitize-pdf',
|
||||||
|
'remove-restrictions',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const simpleTools = [
|
export const simpleTools = [
|
||||||
'encrypt', 'decrypt', 'change-permissions', 'pdf-to-markdown', 'word-to-pdf',
|
'encrypt',
|
||||||
|
'decrypt',
|
||||||
|
'change-permissions',
|
||||||
|
'pdf-to-markdown',
|
||||||
|
'word-to-pdf',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const multiFileTools = [
|
export const multiFileTools = [
|
||||||
'merge', 'pdf-to-zip', 'jpg-to-pdf', 'png-to-pdf', 'webp-to-pdf', 'image-to-pdf', 'svg-to-pdf', 'bmp-to-pdf', 'heic-to-pdf', 'tiff-to-pdf',
|
'merge',
|
||||||
|
'pdf-to-zip',
|
||||||
|
'jpg-to-pdf',
|
||||||
|
'png-to-pdf',
|
||||||
|
'webp-to-pdf',
|
||||||
|
'image-to-pdf',
|
||||||
|
'svg-to-pdf',
|
||||||
|
'bmp-to-pdf',
|
||||||
|
'heic-to-pdf',
|
||||||
|
'tiff-to-pdf',
|
||||||
|
'alternate-merge',
|
||||||
|
'linearize',
|
||||||
|
'reverse-pages',
|
||||||
];
|
];
|
||||||
@@ -1,22 +1,104 @@
|
|||||||
export const tesseractLanguages = {
|
export const tesseractLanguages = {
|
||||||
"eng": "English", "afr": "Afrikaans", "amh": "Amharic", "ara": "Arabic", "asm": "Assamese", "aze": "Azerbaijani",
|
eng: 'English',
|
||||||
"aze_cyrl": "Azerbaijani - Cyrillic", "bel": "Belarusian", "ben": "Bengali", "bod": "Tibetan", "bos": "Bosnian",
|
afr: 'Afrikaans',
|
||||||
"bul": "Bulgarian", "cat": "Catalan; Valencian", "ceb": "Cebuano", "ces": "Czech", "chi_sim": "Chinese - Simplified",
|
amh: 'Amharic',
|
||||||
"chi_tra": "Chinese - Traditional", "chr": "Cherokee", "cym": "Welsh", "dan": "Danish", "deu": "German",
|
ara: 'Arabic',
|
||||||
"dzo": "Dzongkha", "ell": "Greek, Modern (1453-)", "enm": "English, Middle (1100-1500)", "epo": "Esperanto",
|
asm: 'Assamese',
|
||||||
"est": "Estonian", "eus": "Basque", "fas": "Persian", "fin": "Finnish", "fra": "French", "frk": "German Fraktur",
|
aze: 'Azerbaijani',
|
||||||
"frm": "French, Middle (ca. 1400-1600)", "gle": "Irish", "glg": "Galician", "grc": "Greek, Ancient (-1453)",
|
aze_cyrl: 'Azerbaijani - Cyrillic',
|
||||||
"guj": "Gujarati", "hat": "Haitian; Haitian Creole", "heb": "Hebrew", "hin": "Hindi", "hrv": "Croatian",
|
bel: 'Belarusian',
|
||||||
"hun": "Hungarian", "iku": "Inuktitut", "ind": "Indonesian", "isl": "Icelandic", "ita": "Italian",
|
ben: 'Bengali',
|
||||||
"ita_old": "Italian - Old", "jav": "Javanese", "jpn": "Japanese", "kan": "Kannada", "kat": "Georgian",
|
bod: 'Tibetan',
|
||||||
"kat_old": "Georgian - Old", "kaz": "Kazakh", "khm": "Central Khmer", "kir": "Kirghiz; Kyrgyz",
|
bos: 'Bosnian',
|
||||||
"kor": "Korean", "kur": "Kurdish", "lao": "Lao", "lat": "Latin", "lav": "Latvian", "lit": "Lithuanian",
|
bul: 'Bulgarian',
|
||||||
"mal": "Malayalam", "mar": "Marathi", "mkd": "Macedonian", "mlt": "Maltese", "msa": "Malay", "mya": "Burmese",
|
cat: 'Catalan; Valencian',
|
||||||
"nep": "Nepali", "nld": "Dutch; Flemish", "nor": "Norwegian", "ori": "Oriya", "pan": "Panjabi; Punjabi",
|
ceb: 'Cebuano',
|
||||||
"pol": "Polish", "por": "Portuguese", "pus": "Pushto; Pashto", "ron": "Romanian; Moldavian; Moldovan",
|
ces: 'Czech',
|
||||||
"rus": "Russian", "san": "Sanskrit", "sin": "Sinhala; Sinhalese", "slk": "Slovak", "slv": "Slovenian",
|
chi_sim: 'Chinese - Simplified',
|
||||||
"spa": "Spanish; Castilian", "spa_old": "Spanish; Castilian - Old", "sqi": "Albanian", "srp": "Serbian",
|
chi_tra: 'Chinese - Traditional',
|
||||||
"srp_latn": "Serbian - Latin", "swa": "Swahili", "swe": "Swedish", "syr": "Syriac", "tam": "Tamil", "tel": "Telugu",
|
chr: 'Cherokee',
|
||||||
"tgk": "Tajik", "tgl": "Tagalog", "tha": "Thai", "tir": "Tigrinya", "tur": "Turkish", "uig": "Uighur; Uyghur",
|
cym: 'Welsh',
|
||||||
"ukr": "Ukrainian", "urd": "Urdu", "uzb": "Uzbek", "uzb_cyrl": "Uzbek - Cyrillic", "vie": "Vietnamese", "yid": "Yiddish"
|
dan: 'Danish',
|
||||||
|
deu: 'German',
|
||||||
|
dzo: 'Dzongkha',
|
||||||
|
ell: 'Greek, Modern (1453-)',
|
||||||
|
enm: 'English, Middle (1100-1500)',
|
||||||
|
epo: 'Esperanto',
|
||||||
|
est: 'Estonian',
|
||||||
|
eus: 'Basque',
|
||||||
|
fas: 'Persian',
|
||||||
|
fin: 'Finnish',
|
||||||
|
fra: 'French',
|
||||||
|
frk: 'German Fraktur',
|
||||||
|
frm: 'French, Middle (ca. 1400-1600)',
|
||||||
|
gle: 'Irish',
|
||||||
|
glg: 'Galician',
|
||||||
|
grc: 'Greek, Ancient (-1453)',
|
||||||
|
guj: 'Gujarati',
|
||||||
|
hat: 'Haitian; Haitian Creole',
|
||||||
|
heb: 'Hebrew',
|
||||||
|
hin: 'Hindi',
|
||||||
|
hrv: 'Croatian',
|
||||||
|
hun: 'Hungarian',
|
||||||
|
iku: 'Inuktitut',
|
||||||
|
ind: 'Indonesian',
|
||||||
|
isl: 'Icelandic',
|
||||||
|
ita: 'Italian',
|
||||||
|
ita_old: 'Italian - Old',
|
||||||
|
jav: 'Javanese',
|
||||||
|
jpn: 'Japanese',
|
||||||
|
kan: 'Kannada',
|
||||||
|
kat: 'Georgian',
|
||||||
|
kat_old: 'Georgian - Old',
|
||||||
|
kaz: 'Kazakh',
|
||||||
|
khm: 'Central Khmer',
|
||||||
|
kir: 'Kirghiz; Kyrgyz',
|
||||||
|
kor: 'Korean',
|
||||||
|
kur: 'Kurdish',
|
||||||
|
lao: 'Lao',
|
||||||
|
lat: 'Latin',
|
||||||
|
lav: 'Latvian',
|
||||||
|
lit: 'Lithuanian',
|
||||||
|
mal: 'Malayalam',
|
||||||
|
mar: 'Marathi',
|
||||||
|
mkd: 'Macedonian',
|
||||||
|
mlt: 'Maltese',
|
||||||
|
msa: 'Malay',
|
||||||
|
mya: 'Burmese',
|
||||||
|
nep: 'Nepali',
|
||||||
|
nld: 'Dutch; Flemish',
|
||||||
|
nor: 'Norwegian',
|
||||||
|
ori: 'Oriya',
|
||||||
|
pan: 'Panjabi; Punjabi',
|
||||||
|
pol: 'Polish',
|
||||||
|
por: 'Portuguese',
|
||||||
|
pus: 'Pushto; Pashto',
|
||||||
|
ron: 'Romanian; Moldavian; Moldovan',
|
||||||
|
rus: 'Russian',
|
||||||
|
san: 'Sanskrit',
|
||||||
|
sin: 'Sinhala; Sinhalese',
|
||||||
|
slk: 'Slovak',
|
||||||
|
slv: 'Slovenian',
|
||||||
|
spa: 'Spanish; Castilian',
|
||||||
|
spa_old: 'Spanish; Castilian - Old',
|
||||||
|
sqi: 'Albanian',
|
||||||
|
srp: 'Serbian',
|
||||||
|
srp_latn: 'Serbian - Latin',
|
||||||
|
swa: 'Swahili',
|
||||||
|
swe: 'Swedish',
|
||||||
|
syr: 'Syriac',
|
||||||
|
tam: 'Tamil',
|
||||||
|
tel: 'Telugu',
|
||||||
|
tgk: 'Tajik',
|
||||||
|
tgl: 'Tagalog',
|
||||||
|
tha: 'Thai',
|
||||||
|
tir: 'Tigrinya',
|
||||||
|
tur: 'Turkish',
|
||||||
|
uig: 'Uighur; Uyghur',
|
||||||
|
ukr: 'Ukrainian',
|
||||||
|
urd: 'Urdu',
|
||||||
|
uzb: 'Uzbek',
|
||||||
|
uzb_cyrl: 'Uzbek - Cyrillic',
|
||||||
|
vie: 'Vietnamese',
|
||||||
|
yid: 'Yiddish',
|
||||||
};
|
};
|
||||||
+418
-69
@@ -3,103 +3,452 @@ export const categories = [
|
|||||||
{
|
{
|
||||||
name: 'Popular Tools',
|
name: 'Popular Tools',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
|
{
|
||||||
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
|
id: 'merge',
|
||||||
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
|
name: 'Merge PDF',
|
||||||
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs' },
|
icon: 'combine',
|
||||||
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
|
subtitle: 'Combine multiple PDFs into one file.',
|
||||||
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
|
},
|
||||||
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
{
|
||||||
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
|
id: 'split',
|
||||||
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
|
name: 'Split PDF',
|
||||||
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
|
icon: 'scissors',
|
||||||
]
|
subtitle: 'Extract a range of pages into a new PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compress',
|
||||||
|
name: 'Compress PDF',
|
||||||
|
icon: 'zap',
|
||||||
|
subtitle: 'Reduce the file size of your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit',
|
||||||
|
name: 'PDF Editor',
|
||||||
|
icon: 'pocket-knife',
|
||||||
|
subtitle:
|
||||||
|
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jpg-to-pdf',
|
||||||
|
name: 'JPG to PDF',
|
||||||
|
icon: 'image-up',
|
||||||
|
subtitle: 'Create a PDF from one or more JPG images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sign-pdf',
|
||||||
|
name: 'Sign PDF',
|
||||||
|
icon: 'pen-tool',
|
||||||
|
subtitle: 'Draw, type, or upload your signature.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cropper',
|
||||||
|
name: 'Crop PDF',
|
||||||
|
icon: 'crop',
|
||||||
|
subtitle: 'Trim the margins of every page in your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'extract-pages',
|
||||||
|
name: 'Extract Pages',
|
||||||
|
icon: 'ungroup',
|
||||||
|
subtitle: 'Save a selection of pages as new files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'duplicate-organize',
|
||||||
|
name: 'Duplicate & Organize',
|
||||||
|
icon: 'files',
|
||||||
|
subtitle: 'Duplicate, reorder, and delete pages.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delete-pages',
|
||||||
|
name: 'Delete Pages',
|
||||||
|
icon: 'trash-2',
|
||||||
|
subtitle: 'Remove specific pages from your document.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Edit & Annotate',
|
name: 'Edit & Annotate',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.' },
|
{
|
||||||
|
id: 'edit',
|
||||||
|
name: 'PDF Editor',
|
||||||
|
icon: 'pocket-knife',
|
||||||
|
subtitle:
|
||||||
|
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.',
|
||||||
|
},
|
||||||
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
||||||
{ id: 'add-page-numbers', name: 'Page Numbers', icon: 'list-ordered', subtitle: 'Insert page numbers into your document.' },
|
{
|
||||||
{ id: 'add-watermark', name: 'Add Watermark', icon: 'droplets', subtitle: 'Stamp text or an image over your PDF pages.' },
|
id: 'add-page-numbers',
|
||||||
{ id: 'add-header-footer', name: 'Header & Footer', icon: 'pilcrow', subtitle: 'Add text to the top and bottom of pages.' },
|
name: 'Page Numbers',
|
||||||
{ id: 'invert-colors', name: 'Invert Colors', icon: 'contrast', subtitle: 'Create a "dark mode" version of your PDF.' },
|
icon: 'list-ordered',
|
||||||
{ id: 'change-background-color', name: 'Background Color', icon: 'palette', subtitle: 'Change the background color of your PDF.' },
|
subtitle: 'Insert page numbers into your document.',
|
||||||
{ id: 'change-text-color', name: 'Change Text Color', icon: 'type', subtitle: 'Change the color of text in your PDF.' },
|
},
|
||||||
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
|
{
|
||||||
{ id: 'remove-annotations', name: 'Remove Annotations', icon: 'eraser', subtitle: 'Strip comments, highlights, and links.' },
|
id: 'add-watermark',
|
||||||
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
name: 'Add Watermark',
|
||||||
{ id: 'form-filler', name: 'PDF Form Filler', icon: 'square-pen', subtitle: 'Fill in forms directly in the browser.' },
|
icon: 'droplets',
|
||||||
]
|
subtitle: 'Stamp text or an image over your PDF pages.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-header-footer',
|
||||||
|
name: 'Header & Footer',
|
||||||
|
icon: 'pilcrow',
|
||||||
|
subtitle: 'Add text to the top and bottom of pages.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'invert-colors',
|
||||||
|
name: 'Invert Colors',
|
||||||
|
icon: 'contrast',
|
||||||
|
subtitle: 'Create a "dark mode" version of your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'change-background-color',
|
||||||
|
name: 'Background Color',
|
||||||
|
icon: 'palette',
|
||||||
|
subtitle: 'Change the background color of your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'change-text-color',
|
||||||
|
name: 'Change Text Color',
|
||||||
|
icon: 'type',
|
||||||
|
subtitle: 'Change the color of text in your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sign-pdf',
|
||||||
|
name: 'Sign PDF',
|
||||||
|
icon: 'pen-tool',
|
||||||
|
subtitle: 'Draw, type, or upload your signature.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-annotations',
|
||||||
|
name: 'Remove Annotations',
|
||||||
|
icon: 'eraser',
|
||||||
|
subtitle: 'Strip comments, highlights, and links.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cropper',
|
||||||
|
name: 'Crop PDF',
|
||||||
|
icon: 'crop',
|
||||||
|
subtitle: 'Trim the margins of every page in your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'form-filler',
|
||||||
|
name: 'PDF Form Filler',
|
||||||
|
icon: 'square-pen',
|
||||||
|
subtitle: 'Fill in forms directly in the browser.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-blank-pages',
|
||||||
|
name: 'Remove Blank Pages',
|
||||||
|
icon: 'file-minus-2',
|
||||||
|
subtitle: 'Automatically detect and delete blank pages.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Convert to PDF',
|
name: 'Convert to PDF',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'image-to-pdf', name: 'Image to PDF', icon: 'images', subtitle: 'Combine various images into one PDF.' },
|
{
|
||||||
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
|
id: 'image-to-pdf',
|
||||||
{ id: 'png-to-pdf', name: 'PNG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more PNG images.' },
|
name: 'Image to PDF',
|
||||||
{ id: 'webp-to-pdf', name: 'WebP to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more WebP images.' },
|
icon: 'images',
|
||||||
{ id: 'svg-to-pdf', name: 'SVG to PDF', icon: 'pen-tool', subtitle: 'Create a PDF from one or more SVG images.' },
|
subtitle: 'Combine various images into one PDF.',
|
||||||
{ id: 'bmp-to-pdf', name: 'BMP to PDF', icon: 'image', subtitle: 'Create a PDF from one or more BMP images.' },
|
},
|
||||||
{ id: 'heic-to-pdf', name: 'HEIC to PDF', icon: 'smartphone', subtitle: 'Create a PDF from one or more HEIC images.' },
|
{
|
||||||
{ id: 'tiff-to-pdf', name: 'TIFF to PDF', icon: 'layers', subtitle: 'Create a PDF from one or more TIFF images.' },
|
id: 'jpg-to-pdf',
|
||||||
{ id: 'txt-to-pdf', name: 'Text to PDF', icon: 'file-pen', subtitle: 'Convert a plain text file into a PDF.' },
|
name: 'JPG to PDF',
|
||||||
|
icon: 'image-up',
|
||||||
|
subtitle: 'Create a PDF from one or more JPG images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'png-to-pdf',
|
||||||
|
name: 'PNG to PDF',
|
||||||
|
icon: 'image-up',
|
||||||
|
subtitle: 'Create a PDF from one or more PNG images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'webp-to-pdf',
|
||||||
|
name: 'WebP to PDF',
|
||||||
|
icon: 'image-up',
|
||||||
|
subtitle: 'Create a PDF from one or more WebP images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'svg-to-pdf',
|
||||||
|
name: 'SVG to PDF',
|
||||||
|
icon: 'pen-tool',
|
||||||
|
subtitle: 'Create a PDF from one or more SVG images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bmp-to-pdf',
|
||||||
|
name: 'BMP to PDF',
|
||||||
|
icon: 'image',
|
||||||
|
subtitle: 'Create a PDF from one or more BMP images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'heic-to-pdf',
|
||||||
|
name: 'HEIC to PDF',
|
||||||
|
icon: 'smartphone',
|
||||||
|
subtitle: 'Create a PDF from one or more HEIC images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tiff-to-pdf',
|
||||||
|
name: 'TIFF to PDF',
|
||||||
|
icon: 'layers',
|
||||||
|
subtitle: 'Create a PDF from one or more TIFF images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'txt-to-pdf',
|
||||||
|
name: 'Text to PDF',
|
||||||
|
icon: 'file-pen',
|
||||||
|
subtitle: 'Convert a plain text file into a PDF.',
|
||||||
|
},
|
||||||
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
|
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
|
||||||
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
|
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
|
||||||
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
|
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Convert from PDF',
|
name: 'Convert from PDF',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'pdf-to-jpg', name: 'PDF to JPG', icon: 'file-image', subtitle: 'Convert each PDF page into a JPG image.' },
|
{
|
||||||
{ id: 'pdf-to-png', name: 'PDF to PNG', icon: 'file-image', subtitle: 'Convert each PDF page into a PNG image.' },
|
id: 'pdf-to-jpg',
|
||||||
{ id: 'pdf-to-webp', name: 'PDF to WebP', icon: 'file-image', subtitle: 'Convert each PDF page into a WebP image.' },
|
name: 'PDF to JPG',
|
||||||
{ id: 'pdf-to-bmp', name: 'PDF to BMP', icon: 'file-image', subtitle: 'Convert each PDF page into a BMP image.' },
|
icon: 'file-image',
|
||||||
{ id: 'pdf-to-tiff', name: 'PDF to TIFF', icon: 'file-image', subtitle: 'Convert each PDF page into a TIFF image.' },
|
subtitle: 'Convert each PDF page into a JPG image.',
|
||||||
{ id: 'pdf-to-greyscale', name: 'PDF to Greyscale', icon: 'palette', subtitle: 'Convert all colors to black and white.' },
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-png',
|
||||||
|
name: 'PDF to PNG',
|
||||||
|
icon: 'file-image',
|
||||||
|
subtitle: 'Convert each PDF page into a PNG image.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-webp',
|
||||||
|
name: 'PDF to WebP',
|
||||||
|
icon: 'file-image',
|
||||||
|
subtitle: 'Convert each PDF page into a WebP image.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-bmp',
|
||||||
|
name: 'PDF to BMP',
|
||||||
|
icon: 'file-image',
|
||||||
|
subtitle: 'Convert each PDF page into a BMP image.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-tiff',
|
||||||
|
name: 'PDF to TIFF',
|
||||||
|
icon: 'file-image',
|
||||||
|
subtitle: 'Convert each PDF page into a TIFF image.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-greyscale',
|
||||||
|
name: 'PDF to Greyscale',
|
||||||
|
icon: 'palette',
|
||||||
|
subtitle: 'Convert all colors to black and white.',
|
||||||
|
},
|
||||||
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
|
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Organize & Manage',
|
name: 'Organize & Manage',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'ocr-pdf', name: 'OCR PDF', icon: 'scan-text', subtitle: 'Make a PDF searchable and copyable.' },
|
{
|
||||||
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
|
id: 'ocr-pdf',
|
||||||
{ id: 'organize', name: 'Organize PDF', icon: 'grip', subtitle: 'Reorder pages by dragging and dropping.' },
|
name: 'OCR PDF',
|
||||||
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
|
icon: 'scan-text',
|
||||||
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
|
subtitle: 'Make a PDF searchable and copyable.',
|
||||||
{ id: 'split-in-half', name: 'Divide Pages', icon: 'table-columns-split', subtitle: 'Divide pages horizontally or vertically.' },
|
},
|
||||||
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
|
{
|
||||||
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
|
id: 'merge',
|
||||||
{ id: 'add-blank-page', name: 'Add Blank Page', icon: 'file-plus-2', subtitle: 'Insert an empty page anywhere in your PDF.' },
|
name: 'Merge PDF',
|
||||||
{ id: 'reverse-pages', name: 'Reverse Pages', icon: 'arrow-down-z-a', subtitle: 'Flip the order of all pages in your document.' },
|
icon: 'combine',
|
||||||
{ id: 'rotate', name: 'Rotate PDF', icon: 'rotate-cw', subtitle: 'Turn pages in 90-degree increments.' },
|
subtitle: 'Combine multiple PDFs into one file.',
|
||||||
{ id: 'n-up', name: 'N-Up PDF', icon: 'layout-grid', subtitle: 'Arrange multiple pages onto a single sheet.' },
|
},
|
||||||
{ id: 'combine-single-page', name: 'Combine to Single Page', icon: 'unfold-vertical', subtitle: 'Stitch all pages into one continuous scroll.' },
|
{
|
||||||
{ id: 'view-metadata', name: 'View Metadata', icon: 'info', subtitle: 'Inspect the hidden properties of your PDF.' },
|
id: 'alternate-merge',
|
||||||
{ id: 'edit-metadata', name: 'Edit Metadata', icon: 'file-cog', subtitle: 'Change the author, title, and other properties.' },
|
name: 'Alternate & Mix Pages',
|
||||||
{ id: 'pdf-to-zip', name: 'PDFs to ZIP', icon: 'stretch-horizontal', subtitle: 'Package multiple PDF files into a ZIP archive.' },
|
icon: 'shuffle',
|
||||||
{ id: 'compare-pdfs', name: 'Compare PDFs', icon: 'git-compare', subtitle: 'Compare two PDFs side by side.' },
|
subtitle: 'Combine PDFs by alternating pages from each.',
|
||||||
{ id: 'posterize', name: 'Posterize PDF', icon: 'layout-grid', subtitle: 'Split a large page into multiple smaller pages.' },
|
},
|
||||||
]
|
{
|
||||||
|
id: 'organize',
|
||||||
|
name: 'Organize PDF',
|
||||||
|
icon: 'grip',
|
||||||
|
subtitle: 'Reorder pages by dragging and dropping.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'duplicate-organize',
|
||||||
|
name: 'Duplicate & Organize',
|
||||||
|
icon: 'files',
|
||||||
|
subtitle: 'Duplicate, reorder, and delete pages.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-attachments',
|
||||||
|
name: 'Add Attachments',
|
||||||
|
icon: 'paperclip',
|
||||||
|
subtitle: 'Embed one or more files into your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split',
|
||||||
|
name: 'Split PDF',
|
||||||
|
icon: 'scissors',
|
||||||
|
subtitle: 'Extract a range of pages into a new PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-in-half',
|
||||||
|
name: 'Divide Pages',
|
||||||
|
icon: 'table-columns-split',
|
||||||
|
subtitle: 'Divide pages horizontally or vertically.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'extract-pages',
|
||||||
|
name: 'Extract Pages',
|
||||||
|
icon: 'ungroup',
|
||||||
|
subtitle: 'Save a selection of pages as new files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delete-pages',
|
||||||
|
name: 'Delete Pages',
|
||||||
|
icon: 'trash-2',
|
||||||
|
subtitle: 'Remove specific pages from your document.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-blank-page',
|
||||||
|
name: 'Add Blank Page',
|
||||||
|
icon: 'file-plus-2',
|
||||||
|
subtitle: 'Insert an empty page anywhere in your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reverse-pages',
|
||||||
|
name: 'Reverse Pages',
|
||||||
|
icon: 'arrow-down-z-a',
|
||||||
|
subtitle: 'Flip the order of all pages in your document.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rotate',
|
||||||
|
name: 'Rotate PDF',
|
||||||
|
icon: 'rotate-cw',
|
||||||
|
subtitle: 'Turn pages in 90-degree increments.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'n-up',
|
||||||
|
name: 'N-Up PDF',
|
||||||
|
icon: 'layout-grid',
|
||||||
|
subtitle: 'Arrange multiple pages onto a single sheet.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'combine-single-page',
|
||||||
|
name: 'Combine to Single Page',
|
||||||
|
icon: 'unfold-vertical',
|
||||||
|
subtitle: 'Stitch all pages into one continuous scroll.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'view-metadata',
|
||||||
|
name: 'View Metadata',
|
||||||
|
icon: 'info',
|
||||||
|
subtitle: 'Inspect the hidden properties of your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit-metadata',
|
||||||
|
name: 'Edit Metadata',
|
||||||
|
icon: 'file-cog',
|
||||||
|
subtitle: 'Change the author, title, and other properties.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-zip',
|
||||||
|
name: 'PDFs to ZIP',
|
||||||
|
icon: 'stretch-horizontal',
|
||||||
|
subtitle: 'Package multiple PDF files into a ZIP archive.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compare-pdfs',
|
||||||
|
name: 'Compare PDFs',
|
||||||
|
icon: 'git-compare',
|
||||||
|
subtitle: 'Compare two PDFs side by side.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'posterize',
|
||||||
|
name: 'Posterize PDF',
|
||||||
|
icon: 'notepad-text-dashed',
|
||||||
|
subtitle: 'Split a large page into multiple smaller pages.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Optimize & Repair',
|
name: 'Optimize & Repair',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
|
{
|
||||||
{ id: 'fix-dimensions', name: 'Fix Page Size', icon: 'ruler-dimension-line', subtitle: 'Standardize all pages to a uniform size.' },
|
id: 'compress',
|
||||||
{ id: 'page-dimensions', name: 'Page Dimensions', icon: 'ruler', subtitle: 'Analyze page size, orientation, and units.' },
|
name: 'Compress PDF',
|
||||||
]
|
icon: 'zap',
|
||||||
|
subtitle: 'Reduce the file size of your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fix-dimensions',
|
||||||
|
name: 'Fix Page Size',
|
||||||
|
icon: 'ruler-dimension-line',
|
||||||
|
subtitle: 'Standardize all pages to a uniform size.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'linearize',
|
||||||
|
name: 'Linearize PDF',
|
||||||
|
icon: 'gauge',
|
||||||
|
subtitle: 'Optimize PDF for fast web viewing.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-dimensions',
|
||||||
|
name: 'Page Dimensions',
|
||||||
|
icon: 'ruler',
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Secure PDF',
|
name: 'Secure PDF',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'encrypt', name: 'Encrypt PDF', icon: 'lock', subtitle: 'Add a password to protect your PDF.' },
|
{
|
||||||
{ id: 'decrypt', name: 'Decrypt PDF', icon: 'unlock', subtitle: 'Remove password protection from a PDF.' },
|
id: 'encrypt',
|
||||||
{ id: 'flatten', name: 'Flatten PDF', icon: 'layers', subtitle: 'Make form fields and annotations non-editable.' },
|
name: 'Encrypt PDF',
|
||||||
{ id: 'remove-metadata', name: 'Remove Metadata', icon: 'file-x', subtitle: 'Strip hidden data from your PDF.' },
|
icon: 'lock',
|
||||||
{ id: 'change-permissions', name: 'Change Permissions', icon: 'shield-check', subtitle: 'Set or change user permissions on a 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',
|
||||||
|
name: 'Decrypt PDF',
|
||||||
|
icon: 'unlock',
|
||||||
|
subtitle: 'Unlock PDF by removing password protection.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'flatten',
|
||||||
|
name: 'Flatten PDF',
|
||||||
|
icon: 'layers',
|
||||||
|
subtitle: 'Make form fields and annotations non-editable.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-metadata',
|
||||||
|
name: 'Remove Metadata',
|
||||||
|
icon: 'file-x',
|
||||||
|
subtitle: 'Strip hidden data from your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'change-permissions',
|
||||||
|
name: 'Change Permissions',
|
||||||
|
icon: 'shield-check',
|
||||||
|
subtitle: 'Set or change user permissions on a PDF.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
+318
-64
@@ -1,13 +1,24 @@
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js';
|
import {
|
||||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
showLoader,
|
||||||
|
hideLoader,
|
||||||
|
showAlert,
|
||||||
|
renderPageThumbnails,
|
||||||
|
renderFileDisplay,
|
||||||
|
switchView,
|
||||||
|
} from '../ui.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';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { multiFileTools, simpleTools, singlePdfLoadTools } from '../config/pdf-tools.js';
|
import {
|
||||||
|
multiFileTools,
|
||||||
|
simpleTools,
|
||||||
|
singlePdfLoadTools,
|
||||||
|
} from '../config/pdf-tools.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
async function handleSinglePdfUpload(toolId, file) {
|
async function handleSinglePdfUpload(toolId, file) {
|
||||||
@@ -15,17 +26,27 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
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 (state.pdfDoc.isEncrypted && toolId !== 'decrypt' && toolId !== 'change-permissions') {
|
if (
|
||||||
showAlert('Protected PDF', 'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.');
|
state.pdfDoc.isEncrypted &&
|
||||||
|
toolId !== 'decrypt' &&
|
||||||
|
toolId !== 'change-permissions' &&
|
||||||
|
toolId !== 'remove-restrictions'
|
||||||
|
) {
|
||||||
|
showAlert(
|
||||||
|
'Protected PDF',
|
||||||
|
'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.'
|
||||||
|
);
|
||||||
switchView('grid');
|
switchView('grid');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsDiv = document.querySelector('[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]');
|
const optionsDiv = document.querySelector(
|
||||||
|
'[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]'
|
||||||
|
);
|
||||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
@@ -34,34 +55,52 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
processBtn.classList.remove('hidden');
|
processBtn.classList.remove('hidden');
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
const func =
|
||||||
|
typeof logic.process === 'function' ? logic.process : logic;
|
||||||
processBtn.onclick = func;
|
processBtn.onclick = func;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['split', 'delete-pages', 'add-blank-page', 'extract-pages', 'add-header-footer'].includes(toolId)) {
|
if (
|
||||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
|
[
|
||||||
|
'split',
|
||||||
|
'delete-pages',
|
||||||
|
'add-blank-page',
|
||||||
|
'extract-pages',
|
||||||
|
'add-header-footer',
|
||||||
|
].includes(toolId)
|
||||||
|
) {
|
||||||
|
document.getElementById('total-pages').textContent = state.pdfDoc
|
||||||
|
.getPageCount()
|
||||||
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'organize' || toolId === 'rotate') {
|
if (toolId === 'organize' || toolId === 'rotate') {
|
||||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||||
|
|
||||||
if (toolId === 'rotate') {
|
if (toolId === 'rotate') {
|
||||||
const rotateAllControls = document.getElementById('rotate-all-controls');
|
const rotateAllControls = document.getElementById(
|
||||||
|
'rotate-all-controls'
|
||||||
|
);
|
||||||
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
||||||
const rotateAllRightBtn = document.getElementById('rotate-all-right-btn');
|
const rotateAllRightBtn = document.getElementById(
|
||||||
|
'rotate-all-right-btn'
|
||||||
|
);
|
||||||
|
|
||||||
rotateAllControls.classList.remove('hidden');
|
rotateAllControls.classList.remove('hidden');
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const rotateAll = (direction) => {
|
const rotateAll = (direction) => {
|
||||||
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
||||||
const currentRotation = parseInt((item as HTMLElement).dataset.rotation || '0');
|
const currentRotation = parseInt(
|
||||||
const newRotation = (currentRotation + (direction * 90) + 360) % 360;
|
(item as HTMLElement).dataset.rotation || '0'
|
||||||
|
);
|
||||||
|
const newRotation = (currentRotation + direction * 90 + 360) % 360;
|
||||||
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||||
const thumbnail = item.querySelector('canvas, img');
|
const thumbnail = item.querySelector('canvas, img');
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
(thumbnail as HTMLElement).style.transform = `rotate(${newRotation}deg)`;
|
(thumbnail as HTMLElement).style.transform =
|
||||||
|
`rotate(${newRotation}deg)`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -83,13 +122,16 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes as ArrayBuffer }).promise;
|
const pdfjsDoc = await pdfjsLib.getDocument({
|
||||||
const [metadata, fieldObjects] = await Promise.all([
|
data: pdfBytes as ArrayBuffer,
|
||||||
|
}).promise;
|
||||||
|
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
|
||||||
|
|
||||||
@@ -100,7 +142,8 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
h3.className = 'text-lg font-semibold text-white mb-2';
|
h3.className = 'text-lg font-semibold text-white mb-2';
|
||||||
h3.textContent = title;
|
h3.textContent = title;
|
||||||
const ul = document.createElement('ul');
|
const ul = document.createElement('ul');
|
||||||
ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
ul.className =
|
||||||
|
'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||||
wrapper.append(h3, ul);
|
wrapper.append(h3, ul);
|
||||||
return { wrapper, ul };
|
return { wrapper, ul };
|
||||||
};
|
};
|
||||||
@@ -119,7 +162,12 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const parsePdfDate = (pdfDate) => {
|
const parsePdfDate = (pdfDate) => {
|
||||||
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) return pdfDate;
|
if (
|
||||||
|
!pdfDate ||
|
||||||
|
typeof pdfDate !== 'string' ||
|
||||||
|
!pdfDate.startsWith('D:')
|
||||||
|
)
|
||||||
|
return pdfDate;
|
||||||
try {
|
try {
|
||||||
const year = pdfDate.substring(2, 6);
|
const year = pdfDate.substring(2, 6);
|
||||||
const month = pdfDate.substring(6, 8);
|
const month = pdfDate.substring(6, 8);
|
||||||
@@ -127,18 +175,40 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const hour = pdfDate.substring(10, 12);
|
const hour = pdfDate.substring(10, 12);
|
||||||
const minute = pdfDate.substring(12, 14);
|
const minute = pdfDate.substring(12, 14);
|
||||||
const second = pdfDate.substring(14, 16);
|
const second = pdfDate.substring(14, 16);
|
||||||
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
|
return new Date(
|
||||||
} catch { return pdfDate; }
|
`${year}-${month}-${day}T${hour}:${minute}:${second}Z`
|
||||||
|
).toLocaleString();
|
||||||
|
} catch {
|
||||||
|
return pdfDate;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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 ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
|
let displayValue;
|
||||||
value = parsePdfDate(value);
|
|
||||||
|
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]';
|
||||||
}
|
}
|
||||||
infoSection.ul.appendChild(createListItem(key, String(value)));
|
} else if (
|
||||||
|
(key === 'CreationDate' || key === 'ModDate') &&
|
||||||
|
typeof value === 'string'
|
||||||
|
) {
|
||||||
|
displayValue = parsePdfDate(value);
|
||||||
|
} else {
|
||||||
|
displayValue = 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>`;
|
||||||
@@ -150,32 +220,137 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
for (const fieldName in fieldObjects) {
|
for (const fieldName in fieldObjects) {
|
||||||
const field = fieldObjects[fieldName][0];
|
const field = fieldObjects[fieldName][0];
|
||||||
const value = (field as any).fieldValue || '- Not Set -';
|
const value = (field as any).fieldValue || '- Not Set -';
|
||||||
fieldsSection.ul.appendChild(createListItem(fieldName, String(value)));
|
fieldsSection.ul.appendChild(
|
||||||
|
createListItem(fieldName, String(value))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
||||||
}
|
}
|
||||||
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 = 'bg-gray-900 p-4 rounded-lg border border-gray-700';
|
li.className = 'flex flex-col sm:flex-row';
|
||||||
|
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(
|
||||||
|
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');
|
const pre = document.createElement('pre');
|
||||||
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
pre.className =
|
||||||
pre.textContent = String(rawXmpString);
|
'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||||
xmpContainer.appendChild(pre);
|
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');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to view metadata or fields:", e);
|
console.error('Failed to view metadata or fields:', e);
|
||||||
showAlert('Error', 'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
@@ -192,14 +367,23 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
(document.getElementById('meta-title') as HTMLInputElement).value = state.pdfDoc.getTitle() || '';
|
(document.getElementById('meta-title') as HTMLInputElement).value =
|
||||||
(document.getElementById('meta-author') as HTMLInputElement).value = state.pdfDoc.getAuthor() || '';
|
state.pdfDoc.getTitle() || '';
|
||||||
(document.getElementById('meta-subject') as HTMLInputElement).value = state.pdfDoc.getSubject() || '';
|
(document.getElementById('meta-author') as HTMLInputElement).value =
|
||||||
(document.getElementById('meta-keywords') as HTMLInputElement).value = state.pdfDoc.getKeywords() || '';
|
state.pdfDoc.getAuthor() || '';
|
||||||
(document.getElementById('meta-creator') as HTMLInputElement).value = state.pdfDoc.getCreator() || '';
|
(document.getElementById('meta-subject') as HTMLInputElement).value =
|
||||||
(document.getElementById('meta-producer') as HTMLInputElement).value = state.pdfDoc.getProducer() || '';
|
state.pdfDoc.getSubject() || '';
|
||||||
(document.getElementById('meta-creation-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getCreationDate());
|
(document.getElementById('meta-keywords') as HTMLInputElement).value =
|
||||||
(document.getElementById('meta-mod-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getModificationDate());
|
state.pdfDoc.getKeywords() || '';
|
||||||
|
(document.getElementById('meta-creator') as HTMLInputElement).value =
|
||||||
|
state.pdfDoc.getCreator() || '';
|
||||||
|
(document.getElementById('meta-producer') as HTMLInputElement).value =
|
||||||
|
state.pdfDoc.getProducer() || '';
|
||||||
|
(
|
||||||
|
document.getElementById('meta-creation-date') as HTMLInputElement
|
||||||
|
).value = formatDateForInput(state.pdfDoc.getCreationDate());
|
||||||
|
(document.getElementById('meta-mod-date') as HTMLInputElement).value =
|
||||||
|
formatDateForInput(state.pdfDoc.getModificationDate());
|
||||||
|
|
||||||
addBtn.onclick = () => {
|
addBtn.onclick = () => {
|
||||||
const fieldWrapper = document.createElement('div');
|
const fieldWrapper = document.createElement('div');
|
||||||
@@ -208,16 +392,19 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const keyInput = document.createElement('input');
|
const keyInput = document.createElement('input');
|
||||||
keyInput.type = 'text';
|
keyInput.type = 'text';
|
||||||
keyInput.placeholder = 'Key (e.g., Department)';
|
keyInput.placeholder = 'Key (e.g., Department)';
|
||||||
keyInput.className = 'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
keyInput.className =
|
||||||
|
'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||||
|
|
||||||
const valueInput = document.createElement('input');
|
const valueInput = document.createElement('input');
|
||||||
valueInput.type = 'text';
|
valueInput.type = 'text';
|
||||||
valueInput.placeholder = 'Value (e.g., Marketing)';
|
valueInput.placeholder = 'Value (e.g., Marketing)';
|
||||||
valueInput.className = 'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
valueInput.className =
|
||||||
|
'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.type = 'button';
|
removeBtn.type = 'button';
|
||||||
removeBtn.className = 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
|
removeBtn.className =
|
||||||
|
'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
||||||
removeBtn.addEventListener('click', () => fieldWrapper.remove());
|
removeBtn.addEventListener('click', () => fieldWrapper.remove());
|
||||||
|
|
||||||
@@ -231,7 +418,9 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'cropper') {
|
if (toolId === 'cropper') {
|
||||||
document.getElementById('cropper-ui-container').classList.remove('hidden');
|
document
|
||||||
|
.getElementById('cropper-ui-container')
|
||||||
|
.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'page-dimensions') {
|
if (toolId === 'page-dimensions') {
|
||||||
@@ -243,12 +432,62 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Could not load PDF. The file may be invalid, corrupted, or password-protected.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not load PDF. The file may be invalid, corrupted, or password-protected.'
|
||||||
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMultiFileUpload(toolId) {
|
async function handleMultiFileUpload(toolId) {
|
||||||
|
if (
|
||||||
|
toolId === 'merge' ||
|
||||||
|
toolId === 'alternate-merge' ||
|
||||||
|
toolId === 'reverse-pages'
|
||||||
|
) {
|
||||||
|
const pdfFilesUnloaded: File[] = [];
|
||||||
|
|
||||||
|
state.files.forEach((file) => {
|
||||||
|
if (file.type === 'application/pdf') {
|
||||||
|
pdfFilesUnloaded.push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfFilesLoaded = await Promise.all(
|
||||||
|
pdfFilesUnloaded.map(async (file) => {
|
||||||
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
|
const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||||
|
ignoreEncryption: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
pdfDoc,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundEncryptedPDFs = pdfFilesLoaded.filter(
|
||||||
|
(pdf) => pdf.pdfDoc.isEncrypted
|
||||||
|
);
|
||||||
|
|
||||||
|
if (foundEncryptedPDFs.length > 0) {
|
||||||
|
const encryptedPDFFileNames = [];
|
||||||
|
foundEncryptedPDFs.forEach((encryptedPDF) => {
|
||||||
|
encryptedPDFFileNames.push(encryptedPDF.file.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`;
|
||||||
|
|
||||||
|
showAlert('Protected PDFs', errorMessage);
|
||||||
|
|
||||||
|
switchView('grid');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
@@ -261,22 +500,26 @@ function handleMultiFileUpload(toolId) {
|
|||||||
|
|
||||||
if (toolId === 'merge') {
|
if (toolId === 'merge') {
|
||||||
toolLogic.merge.setup();
|
toolLogic.merge.setup();
|
||||||
|
} else if (toolId === 'alternate-merge') {
|
||||||
|
toolLogic['alternate-merge'].setup();
|
||||||
} else if (toolId === 'image-to-pdf') {
|
} else if (toolId === 'image-to-pdf') {
|
||||||
const imageList = document.getElementById('image-list');
|
const imageList = document.getElementById('image-list');
|
||||||
imageList.textContent = ''; // Clear safely
|
imageList.textContent = ''; // Clear safely
|
||||||
|
|
||||||
state.files.forEach(file => {
|
state.files.forEach((file) => {
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = "relative group cursor-move";
|
li.className = 'relative group cursor-move';
|
||||||
li.dataset.fileName = file.name;
|
li.dataset.fileName = file.name;
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.className = "w-full h-full object-cover rounded-md border-2 border-gray-600";
|
img.className =
|
||||||
|
'w-full h-full object-cover rounded-md border-2 border-gray-600';
|
||||||
|
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.className = "absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1";
|
p.className =
|
||||||
|
'absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1';
|
||||||
p.textContent = file.name; // Safe insertion
|
p.textContent = file.name; // Safe insertion
|
||||||
|
|
||||||
li.append(img, p);
|
li.append(img, p);
|
||||||
@@ -314,11 +557,14 @@ export function setupFileInputHandler(toolId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMultiFileTool) {
|
if (isMultiFileTool) {
|
||||||
handleMultiFileUpload(toolId);
|
await handleMultiFileUpload(toolId);
|
||||||
} else if (singlePdfLoadTools.includes(toolId)) {
|
} else if (singlePdfLoadTools.includes(toolId)) {
|
||||||
await handleSinglePdfUpload(toolId, state.files[0]);
|
await handleSinglePdfUpload(toolId, state.files[0]);
|
||||||
} else if (simpleTools.includes(toolId)) {
|
} else if (simpleTools.includes(toolId)) {
|
||||||
const optionsDivId = toolId === 'change-permissions' ? 'permissions-options' : `${toolId}-options`;
|
const optionsDivId =
|
||||||
|
toolId === 'change-permissions'
|
||||||
|
? 'permissions-options'
|
||||||
|
: `${toolId}-options`;
|
||||||
const optionsDiv = document.getElementById(optionsDivId);
|
const optionsDiv = document.getElementById(optionsDivId);
|
||||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
@@ -327,7 +573,8 @@ export function setupFileInputHandler(toolId) {
|
|||||||
processBtn.onclick = () => {
|
processBtn.onclick = () => {
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
const func =
|
||||||
|
typeof logic.process === 'function' ? logic.process : logic;
|
||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -371,7 +618,9 @@ export function setupFileInputHandler(toolId) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fileInput.addEventListener('change', (e) => processFiles(Array.from((e.target as HTMLInputElement).files || [])));
|
fileInput.addEventListener('change', (e) =>
|
||||||
|
processFiles(Array.from((e.target as HTMLInputElement).files || []))
|
||||||
|
);
|
||||||
|
|
||||||
const setupAddMoreButton = () => {
|
const setupAddMoreButton = () => {
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
@@ -394,8 +643,13 @@ export function setupFileInputHandler(toolId) {
|
|||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
if (fileControls) fileControls.classList.add('hidden');
|
if (fileControls) fileControls.classList.add('hidden');
|
||||||
|
|
||||||
const toolSpecificUI = ['file-list', 'page-merge-preview', 'image-list'];
|
const toolSpecificUI = [
|
||||||
toolSpecificUI.forEach(id => {
|
'file-list',
|
||||||
|
'page-merge-preview',
|
||||||
|
'image-list',
|
||||||
|
'alternate-file-list',
|
||||||
|
];
|
||||||
|
toolSpecificUI.forEach((id) => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.textContent = '';
|
if (el) el.textContent = '';
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function setupToolInterface(toolId: any) {
|
|||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
behavior: 'instant' as ScrollBehavior
|
behavior: 'instant' as ScrollBehavior,
|
||||||
});
|
});
|
||||||
|
|
||||||
state.activeTool = toolId;
|
state.activeTool = toolId;
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||||
|
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
|
||||||
|
import { state } from '../state';
|
||||||
|
let attachments: File[] = [];
|
||||||
|
|
||||||
|
export async function addAttachments() {
|
||||||
|
if (!state.pdfDoc) {
|
||||||
|
showAlert('Error', 'Main PDF is not loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attachments.length === 0) {
|
||||||
|
showAlert('No Files', 'Please select at least one file to attach.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Embedding files into PDF...');
|
||||||
|
try {
|
||||||
|
const pdfDoc = state.pdfDoc;
|
||||||
|
|
||||||
|
for (let i = 0; i < attachments.length; i++) {
|
||||||
|
const file = attachments[i];
|
||||||
|
showLoader(`Attaching ${file.name} (${i + 1}/${attachments.length})...`);
|
||||||
|
|
||||||
|
const fileBytes = await readFileAsArrayBuffer(file);
|
||||||
|
|
||||||
|
await pdfDoc.attach(fileBytes as ArrayBuffer, file.name, {
|
||||||
|
mimeType: file.type || 'application/octet-stream',
|
||||||
|
description: `Attached file: ${file.name}`,
|
||||||
|
creationDate: new Date(),
|
||||||
|
modificationDate: new Date(file.lastModified),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([pdfBytes], { type: 'application/pdf' }),
|
||||||
|
`attached-${state.files[0].name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`${attachments.length} file(s) attached successfully.`
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error attaching files:', error);
|
||||||
|
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
clearAttachments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAttachments() {
|
||||||
|
attachments = [];
|
||||||
|
const fileListDiv = document.getElementById('attachment-file-list');
|
||||||
|
const attachmentInput = document.getElementById(
|
||||||
|
'attachment-files-input'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
|
if (fileListDiv) fileListDiv.innerHTML = '';
|
||||||
|
if (attachmentInput) attachmentInput.value = '';
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.disabled = true;
|
||||||
|
processBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isSetup = false; // Prevent duplicate setup
|
||||||
|
|
||||||
|
export function setupAddAttachmentsTool() {
|
||||||
|
if (isSetup) return; // Already set up
|
||||||
|
isSetup = true;
|
||||||
|
|
||||||
|
const optionsDiv = document.getElementById('attachment-options');
|
||||||
|
const attachmentInput = document.getElementById(
|
||||||
|
'attachment-files-input'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const fileListDiv = document.getElementById('attachment-file-list');
|
||||||
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
|
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
|
||||||
|
console.error('Attachment tool UI elements not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
attachmentInput.addEventListener('change', (e) => {
|
||||||
|
const files = (e.target as HTMLInputElement).files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
attachments = Array.from(files);
|
||||||
|
|
||||||
|
fileListDiv.innerHTML = '';
|
||||||
|
attachments.forEach((file) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className =
|
||||||
|
'flex justify-between items-center p-2 bg-gray-800 rounded-md text-white';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('span');
|
||||||
|
nameSpan.className = 'truncate text-sm';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
|
const sizeSpan = document.createElement('span');
|
||||||
|
sizeSpan.className = 'text-xs text-gray-400';
|
||||||
|
sizeSpan.textContent = `${Math.round(file.size / 1024)} KB`;
|
||||||
|
|
||||||
|
div.appendChild(nameSpan);
|
||||||
|
div.appendChild(sizeSpan);
|
||||||
|
fileListDiv.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
processBtn.disabled = false;
|
||||||
|
processBtn.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
clearAttachments();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
processBtn.onclick = addAttachments;
|
||||||
|
}
|
||||||
@@ -6,19 +6,39 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
|||||||
export async function addBlankPage() {
|
export async function addBlankPage() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const pageNumberInput = document.getElementById('page-number').value;
|
const pageNumberInput = document.getElementById('page-number').value;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const pageCountInput = document.getElementById('page-count').value;
|
||||||
|
|
||||||
if (pageNumberInput.trim() === '') {
|
if (pageNumberInput.trim() === '') {
|
||||||
showAlert('Invalid Input', 'Please enter a page number.');
|
showAlert('Invalid Input', 'Please enter a page number.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = parseInt(pageNumberInput);
|
if (pageCountInput.trim() === '') {
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
showAlert('Invalid Input', 'Please enter the number of pages to insert.');
|
||||||
if (isNaN(position) || position < 0 || position > totalPages) {
|
|
||||||
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Adding page...');
|
const position = parseInt(pageNumberInput);
|
||||||
|
const pageCount = parseInt(pageCountInput);
|
||||||
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
|
if (isNaN(position) || position < 0 || position > totalPages) {
|
||||||
|
showAlert(
|
||||||
|
'Invalid Input',
|
||||||
|
`Please enter a number between 0 and ${totalPages}.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(pageCount) || pageCount < 1) {
|
||||||
|
showAlert(
|
||||||
|
'Invalid Input',
|
||||||
|
'Please enter a valid number of pages (1 or more).'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader(`Adding ${pageCount} blank page${pageCount > 1 ? 's' : ''}...`);
|
||||||
try {
|
try {
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
||||||
@@ -32,7 +52,10 @@ export async function addBlankPage() {
|
|||||||
copied.forEach((p: any) => newPdf.addPage(p));
|
copied.forEach((p: any) => newPdf.addPage(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the specified number of blank pages
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
newPdf.addPage([width, height]);
|
newPdf.addPage([width, height]);
|
||||||
|
}
|
||||||
|
|
||||||
if (indicesAfter.length > 0) {
|
if (indicesAfter.length > 0) {
|
||||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
||||||
@@ -40,11 +63,13 @@ export async function addBlankPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'page-added.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
`blank-page${pageCount > 1 ? 's' : ''}-added.pdf`
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not add a blank page.');
|
showAlert('Error', `Could not add blank page${pageCount > 1 ? 's' : ''}.`);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { state } from '../state.js';
|
|||||||
|
|
||||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||||
|
|
||||||
|
|
||||||
export function setupHeaderFooterUI() {
|
export function setupHeaderFooterUI() {
|
||||||
const totalPagesSpan = document.getElementById('total-pages');
|
const totalPagesSpan = document.getElementById('total-pages');
|
||||||
if (totalPagesSpan && state.pdfDoc) {
|
if (totalPagesSpan && state.pdfDoc) {
|
||||||
@@ -48,14 +47,16 @@ export async function addHeaderFooter() {
|
|||||||
// --- 3. Parse page range to determine which pages to modify ---
|
// --- 3. Parse page range to determine which pages to modify ---
|
||||||
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||||
if (indicesToProcess.length === 0) {
|
if (indicesToProcess.length === 0) {
|
||||||
throw new Error("Invalid page range specified. Please check your input (e.g., '1-3, 5').");
|
throw new Error(
|
||||||
|
"Invalid page range specified. Please check your input (e.g., '1-3, 5')."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 4. Define drawing options with new values ---
|
// --- 4. Define drawing options with new values ---
|
||||||
const drawOptions = {
|
const drawOptions = {
|
||||||
font: helveticaFont,
|
font: helveticaFont,
|
||||||
size: fontSize,
|
size: fontSize,
|
||||||
color: rgb(fontColor.r, fontColor.g, fontColor.b)
|
color: rgb(fontColor.r, fontColor.g, fontColor.b),
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 5. Loop over only the selected pages ---
|
// --- 5. Loop over only the selected pages ---
|
||||||
@@ -67,9 +68,8 @@ export async function addHeaderFooter() {
|
|||||||
const pageNumber = pageIndex + 1; // For dynamic text
|
const pageNumber = pageIndex + 1; // For dynamic text
|
||||||
|
|
||||||
// Helper to replace placeholders like {page} and {total}
|
// Helper to replace placeholders like {page} and {total}
|
||||||
const processText = (text: any) => text
|
const processText = (text: any) =>
|
||||||
.replace(/{page}/g, pageNumber)
|
text.replace(/{page}/g, pageNumber).replace(/{total}/g, totalPages);
|
||||||
.replace(/{total}/g, totalPages);
|
|
||||||
|
|
||||||
// Get processed text for the current page
|
// Get processed text for the current page
|
||||||
const processedTexts = {
|
const processedTexts = {
|
||||||
@@ -81,17 +81,73 @@ export async function addHeaderFooter() {
|
|||||||
footerRight: processText(texts.footerRight),
|
footerRight: processText(texts.footerRight),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (processedTexts.headerLeft) page.drawText(processedTexts.headerLeft, { ...drawOptions, x: margin, y: height - margin });
|
if (processedTexts.headerLeft)
|
||||||
if (processedTexts.headerCenter) page.drawText(processedTexts.headerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.headerCenter, fontSize) / 2, y: height - margin });
|
page.drawText(processedTexts.headerLeft, {
|
||||||
if (processedTexts.headerRight) page.drawText(processedTexts.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.headerRight, fontSize), y: height - margin });
|
...drawOptions,
|
||||||
if (processedTexts.footerLeft) page.drawText(processedTexts.footerLeft, { ...drawOptions, x: margin, y: margin });
|
x: margin,
|
||||||
if (processedTexts.footerCenter) page.drawText(processedTexts.footerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.footerCenter, fontSize) / 2, y: margin });
|
y: height - margin,
|
||||||
if (processedTexts.footerRight) page.drawText(processedTexts.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.footerRight, fontSize), y: margin });
|
});
|
||||||
|
if (processedTexts.headerCenter)
|
||||||
|
page.drawText(processedTexts.headerCenter, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width / 2 -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.headerCenter,
|
||||||
|
fontSize
|
||||||
|
) /
|
||||||
|
2,
|
||||||
|
y: height - margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.headerRight)
|
||||||
|
page.drawText(processedTexts.headerRight, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width -
|
||||||
|
margin -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.headerRight,
|
||||||
|
fontSize
|
||||||
|
),
|
||||||
|
y: height - margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.footerLeft)
|
||||||
|
page.drawText(processedTexts.footerLeft, {
|
||||||
|
...drawOptions,
|
||||||
|
x: margin,
|
||||||
|
y: margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.footerCenter)
|
||||||
|
page.drawText(processedTexts.footerCenter, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width / 2 -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.footerCenter,
|
||||||
|
fontSize
|
||||||
|
) /
|
||||||
|
2,
|
||||||
|
y: margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.footerRight)
|
||||||
|
page.drawText(processedTexts.footerRight, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width -
|
||||||
|
margin -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.footerRight,
|
||||||
|
fontSize
|
||||||
|
),
|
||||||
|
y: margin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'header-footer-added.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'header-footer-added.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Could not add header or footer.');
|
showAlert('Error', e.message || 'Could not add header or footer.');
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export async function addPageNumbers() {
|
|||||||
const xOffset = bounds.x || 0;
|
const xOffset = bounds.x || 0;
|
||||||
const yOffset = bounds.y || 0;
|
const yOffset = bounds.y || 0;
|
||||||
|
|
||||||
let pageNumText = (format === 'page_x_of_y') ? `${i + 1} / ${totalPages}` : `${i + 1}`;
|
let pageNumText =
|
||||||
|
format === 'page_x_of_y' ? `${i + 1} / ${totalPages}` : `${i + 1}`;
|
||||||
|
|
||||||
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
|
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
|
||||||
const textHeight = fontSize;
|
const textHeight = fontSize;
|
||||||
@@ -41,18 +42,34 @@ export async function addPageNumbers() {
|
|||||||
const maxMargin = 40;
|
const maxMargin = 40;
|
||||||
const marginPercentage = 0.04;
|
const marginPercentage = 0.04;
|
||||||
|
|
||||||
const horizontalMargin = Math.max(minMargin, Math.min(maxMargin, width * marginPercentage));
|
const horizontalMargin = Math.max(
|
||||||
const verticalMargin = Math.max(minMargin, Math.min(maxMargin, height * marginPercentage));
|
minMargin,
|
||||||
|
Math.min(maxMargin, width * marginPercentage)
|
||||||
|
);
|
||||||
|
const verticalMargin = Math.max(
|
||||||
|
minMargin,
|
||||||
|
Math.min(maxMargin, height * marginPercentage)
|
||||||
|
);
|
||||||
|
|
||||||
// Ensure text doesn't go outside visible page boundaries
|
// Ensure text doesn't go outside visible page boundaries
|
||||||
const safeHorizontalMargin = Math.max(horizontalMargin, textWidth / 2 + 3);
|
const safeHorizontalMargin = Math.max(
|
||||||
|
horizontalMargin,
|
||||||
|
textWidth / 2 + 3
|
||||||
|
);
|
||||||
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
||||||
|
|
||||||
let x, y;
|
let x, y;
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'bottom-center':
|
case 'bottom-center':
|
||||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
Math.min(
|
||||||
|
width - safeHorizontalMargin - textWidth,
|
||||||
|
(width - textWidth) / 2
|
||||||
|
)
|
||||||
|
) + xOffset;
|
||||||
y = safeVerticalMargin + yOffset;
|
y = safeVerticalMargin + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'bottom-left':
|
case 'bottom-left':
|
||||||
@@ -60,11 +77,22 @@ export async function addPageNumbers() {
|
|||||||
y = safeVerticalMargin + yOffset;
|
y = safeVerticalMargin + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'bottom-right':
|
case 'bottom-right':
|
||||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
width - safeHorizontalMargin - textWidth
|
||||||
|
) + xOffset;
|
||||||
y = safeVerticalMargin + yOffset;
|
y = safeVerticalMargin + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'top-center':
|
case 'top-center':
|
||||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
Math.min(
|
||||||
|
width - safeHorizontalMargin - textWidth,
|
||||||
|
(width - textWidth) / 2
|
||||||
|
)
|
||||||
|
) + xOffset;
|
||||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'top-left':
|
case 'top-left':
|
||||||
@@ -72,7 +100,11 @@ export async function addPageNumbers() {
|
|||||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'top-right':
|
case 'top-right':
|
||||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
width - safeHorizontalMargin - textWidth
|
||||||
|
) + xOffset;
|
||||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -82,15 +114,19 @@ export async function addPageNumbers() {
|
|||||||
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
|
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
|
||||||
|
|
||||||
page.drawText(pageNumText, {
|
page.drawText(pageNumText, {
|
||||||
x, y,
|
x,
|
||||||
|
y,
|
||||||
font: helveticaFont,
|
font: helveticaFont,
|
||||||
size: fontSize,
|
size: fontSize,
|
||||||
color: rgb(textColor.r, textColor.g, textColor.b)
|
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'paginated.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'paginated.pdf'
|
||||||
|
);
|
||||||
showAlert('Success', 'Page numbers added successfully!');
|
showAlert('Success', 'Page numbers added successfully!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
+100
-36
@@ -1,15 +1,26 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, hexToRgb } from '../utils/helpers.js';
|
import {
|
||||||
import { state } from '../state.js';
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
hexToRgb,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
|
import { state, resetState } from '../state.js';
|
||||||
|
|
||||||
import { PDFDocument as PDFLibDocument, rgb, degrees, StandardFonts } from 'pdf-lib';
|
import {
|
||||||
|
PDFDocument as PDFLibDocument,
|
||||||
|
rgb,
|
||||||
|
degrees,
|
||||||
|
StandardFonts,
|
||||||
|
} from 'pdf-lib';
|
||||||
|
|
||||||
export function setupWatermarkUI() {
|
export function setupWatermarkUI() {
|
||||||
const watermarkTypeRadios = document.querySelectorAll('input[name="watermark-type"]');
|
const watermarkTypeRadios = document.querySelectorAll(
|
||||||
|
'input[name="watermark-type"]'
|
||||||
|
);
|
||||||
const textOptions = document.getElementById('text-watermark-options');
|
const textOptions = document.getElementById('text-watermark-options');
|
||||||
const imageOptions = document.getElementById('image-watermark-options');
|
const imageOptions = document.getElementById('image-watermark-options');
|
||||||
|
|
||||||
watermarkTypeRadios.forEach(radio => {
|
watermarkTypeRadios.forEach((radio) => {
|
||||||
radio.addEventListener('change', (e) => {
|
radio.addEventListener('change', (e) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||||
if (e.target.value === 'text') {
|
if (e.target.value === 'text') {
|
||||||
@@ -27,25 +38,48 @@ export function setupWatermarkUI() {
|
|||||||
const angleSliderText = document.getElementById('angle-text');
|
const angleSliderText = document.getElementById('angle-text');
|
||||||
const angleValueText = document.getElementById('angle-value-text');
|
const angleValueText = document.getElementById('angle-value-text');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
opacitySliderText.addEventListener(
|
||||||
opacitySliderText.addEventListener('input', () => opacityValueText.textContent = opacitySliderText.value);
|
'input',
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
() =>
|
||||||
angleSliderText.addEventListener('input', () => angleValueText.textContent = angleSliderText.value);
|
(opacityValueText.textContent = (
|
||||||
|
opacitySliderText as HTMLInputElement
|
||||||
|
).value)
|
||||||
|
);
|
||||||
|
|
||||||
|
angleSliderText.addEventListener(
|
||||||
|
'input',
|
||||||
|
() =>
|
||||||
|
(angleValueText.textContent = (angleSliderText as HTMLInputElement).value)
|
||||||
|
);
|
||||||
|
|
||||||
const opacitySliderImage = document.getElementById('opacity-image');
|
const opacitySliderImage = document.getElementById('opacity-image');
|
||||||
const opacityValueImage = document.getElementById('opacity-value-image');
|
const opacityValueImage = document.getElementById('opacity-value-image');
|
||||||
const angleSliderImage = document.getElementById('angle-image');
|
const angleSliderImage = document.getElementById('angle-image');
|
||||||
const angleValueImage = document.getElementById('angle-value-image');
|
const angleValueImage = document.getElementById('angle-value-image');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
opacitySliderImage.addEventListener(
|
||||||
opacitySliderImage.addEventListener('input', () => opacityValueImage.textContent = opacitySliderImage.value);
|
'input',
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
() =>
|
||||||
angleSliderImage.addEventListener('input', () => angleValueImage.textContent = angleSliderImage.value);
|
(opacityValueImage.textContent = (
|
||||||
|
opacitySliderImage as HTMLInputElement
|
||||||
|
).value)
|
||||||
|
);
|
||||||
|
|
||||||
|
angleSliderImage.addEventListener(
|
||||||
|
'input',
|
||||||
|
() =>
|
||||||
|
(angleValueImage.textContent = (
|
||||||
|
angleSliderImage as HTMLInputElement
|
||||||
|
).value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addWatermark() {
|
export async function addWatermark() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
const watermarkType = (
|
||||||
const watermarkType = document.querySelector('input[name="watermark-type"]:checked').value;
|
document.querySelector(
|
||||||
|
'input[name="watermark-type"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
|
||||||
showLoader('Adding watermark...');
|
showLoader('Adding watermark...');
|
||||||
|
|
||||||
@@ -55,10 +89,13 @@ export async function addWatermark() {
|
|||||||
|
|
||||||
if (watermarkType === 'text') {
|
if (watermarkType === 'text') {
|
||||||
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
} else { // 'image'
|
} else {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// 'image'
|
||||||
const imageFile = document.getElementById('image-watermark-input').files[0];
|
const imageFile = (
|
||||||
if (!imageFile) throw new Error('Please select an image file for the watermark.');
|
document.getElementById('image-watermark-input') as HTMLInputElement
|
||||||
|
).files?.[0];
|
||||||
|
if (!imageFile)
|
||||||
|
throw new Error('Please select an image file for the watermark.');
|
||||||
|
|
||||||
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
||||||
if (imageFile.type === 'image/png') {
|
if (imageFile.type === 'image/png') {
|
||||||
@@ -66,7 +103,9 @@ export async function addWatermark() {
|
|||||||
} else if (imageFile.type === 'image/jpeg') {
|
} else if (imageFile.type === 'image/jpeg') {
|
||||||
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
|
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unsupported Image. Please use a PNG or JPG for the watermark.');
|
throw new Error(
|
||||||
|
'Unsupported Image. Please use a PNG or JPG for the watermark.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,16 +115,24 @@ export async function addWatermark() {
|
|||||||
if (watermarkType === 'text') {
|
if (watermarkType === 'text') {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const text = document.getElementById('watermark-text').value;
|
const text = document.getElementById('watermark-text').value;
|
||||||
if (!text.trim()) throw new Error('Please enter text for the watermark.');
|
if (!text.trim())
|
||||||
|
throw new Error('Please enter text for the watermark.');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const fontSize =
|
||||||
const fontSize = parseInt(document.getElementById('font-size').value) || 72;
|
parseInt(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
(document.getElementById('font-size') as HTMLInputElement).value
|
||||||
const angle = parseInt(document.getElementById('angle-text').value) || 0;
|
) || 72;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const angle =
|
||||||
const opacity = parseFloat(document.getElementById('opacity-text').value) || 0.3;
|
parseInt(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
(document.getElementById('angle-text') as HTMLInputElement).value
|
||||||
const colorHex = document.getElementById('text-color').value;
|
) || 0;
|
||||||
|
const opacity =
|
||||||
|
parseFloat(
|
||||||
|
(document.getElementById('opacity-text') as HTMLInputElement).value
|
||||||
|
) || 0.3;
|
||||||
|
const colorHex = (
|
||||||
|
document.getElementById('text-color') as HTMLInputElement
|
||||||
|
).value;
|
||||||
const textColor = hexToRgb(colorHex);
|
const textColor = hexToRgb(colorHex);
|
||||||
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
||||||
|
|
||||||
@@ -98,12 +145,15 @@ export async function addWatermark() {
|
|||||||
opacity: opacity,
|
opacity: opacity,
|
||||||
rotate: degrees(angle),
|
rotate: degrees(angle),
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const angle =
|
||||||
const angle = parseInt(document.getElementById('angle-image').value) || 0;
|
parseInt(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
(document.getElementById('angle-image') as HTMLInputElement).value
|
||||||
const opacity = parseFloat(document.getElementById('opacity-image').value) || 0.3;
|
) || 0;
|
||||||
|
const opacity =
|
||||||
|
parseFloat(
|
||||||
|
(document.getElementById('opacity-image') as HTMLInputElement).value
|
||||||
|
) || 0.3;
|
||||||
|
|
||||||
const scale = 0.5;
|
const scale = 0.5;
|
||||||
const imgWidth = watermarkAsset.width * scale;
|
const imgWidth = watermarkAsset.width * scale;
|
||||||
@@ -121,11 +171,25 @@ export async function addWatermark() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'watermarked.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/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('Error', e.message || 'Could not add the watermark. Please check your inputs.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
e.message || 'Could not add the watermark. Please check your inputs.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
|
const alternateMergeState = {
|
||||||
|
pdfDocs: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function setupAlternateMergeTool() {
|
||||||
|
const optionsDiv = document.getElementById('alternate-merge-options');
|
||||||
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const fileList = document.getElementById('alternate-file-list');
|
||||||
|
|
||||||
|
if (!optionsDiv || !processBtn || !fileList) return;
|
||||||
|
|
||||||
|
optionsDiv.classList.remove('hidden');
|
||||||
|
processBtn.disabled = false;
|
||||||
|
processBtn.onclick = alternateMerge;
|
||||||
|
|
||||||
|
fileList.innerHTML = '';
|
||||||
|
alternateMergeState.pdfDocs = {};
|
||||||
|
|
||||||
|
showLoader('Loading PDF documents...');
|
||||||
|
try {
|
||||||
|
for (const file of state.files) {
|
||||||
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
|
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(
|
||||||
|
pdfBytes as ArrayBuffer,
|
||||||
|
{
|
||||||
|
ignoreEncryption: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
|
||||||
|
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className =
|
||||||
|
'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
|
||||||
|
li.dataset.fileName = file.name;
|
||||||
|
|
||||||
|
const infoDiv = document.createElement('div');
|
||||||
|
infoDiv.className = 'flex items-center gap-2 truncate';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('span');
|
||||||
|
nameSpan.className = 'truncate font-medium text-white';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
|
const pagesSpan = document.createElement('span');
|
||||||
|
pagesSpan.className = 'text-sm text-gray-400 flex-shrink-0';
|
||||||
|
pagesSpan.textContent = `(${pageCount} pages)`;
|
||||||
|
|
||||||
|
infoDiv.append(nameSpan, pagesSpan);
|
||||||
|
|
||||||
|
const dragHandle = document.createElement('div');
|
||||||
|
dragHandle.className =
|
||||||
|
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded';
|
||||||
|
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
||||||
|
|
||||||
|
li.append(infoDiv, dragHandle);
|
||||||
|
fileList.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sortable.create(fileList, {
|
||||||
|
handle: '.drag-handle',
|
||||||
|
animation: 150,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to load one or more PDF files. They may be corrupted or password-protected.'
|
||||||
|
);
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function alternateMerge() {
|
||||||
|
if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
|
||||||
|
showAlert(
|
||||||
|
'Not Enough Files',
|
||||||
|
'Please upload at least two PDF files to alternate and mix.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Alternating and mixing pages...');
|
||||||
|
try {
|
||||||
|
const newPdfDoc = await PDFDocument.create();
|
||||||
|
const fileList = document.getElementById('alternate-file-list');
|
||||||
|
const sortedFileNames = Array.from(fileList.children).map(
|
||||||
|
(li) => (li as HTMLElement).dataset.fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadedDocs = sortedFileNames.map(
|
||||||
|
(name) => alternateMergeState.pdfDocs[name]
|
||||||
|
);
|
||||||
|
const pageCounts = loadedDocs.map((doc) => doc.getPageCount());
|
||||||
|
const maxPages = Math.max(...pageCounts);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxPages; i++) {
|
||||||
|
for (const doc of loadedDocs) {
|
||||||
|
if (i < doc.getPageCount()) {
|
||||||
|
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
|
||||||
|
newPdfDoc.addPage(copiedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedPdfBytes = await newPdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'alternated-mixed.pdf'
|
||||||
|
);
|
||||||
|
showAlert('Success', 'PDFs have been mixed successfully!');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Alternate Merge error:', e);
|
||||||
|
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,9 @@ async function convertImageToPngBytes(file: any) {
|
|||||||
canvas.height = img.height;
|
canvas.height = img.height;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
|
const pngBlob = await new Promise((res) =>
|
||||||
|
canvas.toBlob(res, 'image/png')
|
||||||
|
);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
const pngBytes = await pngBlob.arrayBuffer();
|
||||||
resolve(pngBytes);
|
resolve(pngBytes);
|
||||||
@@ -42,13 +44,24 @@ export async function bmpToPdf() {
|
|||||||
const pngBytes = await convertImageToPngBytes(file);
|
const pngBytes = await convertImageToPngBytes(file);
|
||||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
page.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: pngImage.width,
|
||||||
|
height: pngImage.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_bmps.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_bmps.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert BMP to PDF. One of the files may be invalid.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert BMP to PDF. One of the files may be invalid.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -43,7 +42,10 @@ export async function changeBackgroundColor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'background-changed.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not change the background color.');
|
showAlert('Error', 'Could not change the background color.');
|
||||||
|
|||||||
@@ -1,115 +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 = (document.getElementById('current-password') as HTMLInputElement).value;
|
|
||||||
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement).value;
|
|
||||||
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement).value;
|
|
||||||
|
|
||||||
// An owner password is required to enforce any permissions.
|
|
||||||
if (!newOwnerPassword && (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 {
|
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
const pdfData = await readFileAsArrayBuffer(file);
|
const currentPassword =
|
||||||
|
(document.getElementById('current-password') as HTMLInputElement)?.value ||
|
||||||
|
'';
|
||||||
|
const newUserPassword =
|
||||||
|
(document.getElementById('new-user-password') as HTMLInputElement)?.value ||
|
||||||
|
'';
|
||||||
|
const newOwnerPassword =
|
||||||
|
(document.getElementById('new-owner-password') as HTMLInputElement)
|
||||||
|
?.value || '';
|
||||||
|
|
||||||
|
const inputPath = '/input.pdf';
|
||||||
|
const outputPath = '/output.pdf';
|
||||||
|
let qpdf: any;
|
||||||
|
|
||||||
let pdf;
|
|
||||||
try {
|
try {
|
||||||
pdf = await pdfjsLib.getDocument({
|
showLoader('Initializing...');
|
||||||
data: pdfData as ArrayBuffer,
|
qpdf = await initializeQpdf();
|
||||||
password: currentPassword
|
|
||||||
}).promise;
|
showLoader('Reading PDF...');
|
||||||
} catch (e) {
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
// This catch is specific to password errors in pdf.js
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
if (e.name === 'PasswordException') {
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
hideLoader();
|
|
||||||
showAlert('Incorrect Password', 'The current password you entered is incorrect.');
|
showLoader('Processing PDF permissions...');
|
||||||
return;
|
|
||||||
}
|
const args = [inputPath];
|
||||||
throw e;
|
|
||||||
|
// Add password if provided
|
||||||
|
if (currentPassword) {
|
||||||
|
args.push('--password=' + currentPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
const numPages = pdf.numPages;
|
const shouldEncrypt = newUserPassword || newOwnerPassword;
|
||||||
const pageImages = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= numPages; i++) {
|
if (shouldEncrypt) {
|
||||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
const finalUserPassword = newUserPassword;
|
||||||
const page = await pdf.getPage(i);
|
const finalOwnerPassword = newOwnerPassword;
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
|
||||||
const canvas = document.createElement('canvas');
|
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
const allowPrinting = (
|
||||||
const context = canvas.getContext('2d');
|
document.getElementById('allow-printing') as HTMLInputElement
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
)?.checked;
|
||||||
pageImages.push({
|
const allowCopying = (
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
document.getElementById('allow-copying') as HTMLInputElement
|
||||||
width: viewport.width,
|
)?.checked;
|
||||||
height: viewport.height
|
const allowModifying = (
|
||||||
});
|
document.getElementById('allow-modifying') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
const allowAnnotating = (
|
||||||
|
document.getElementById('allow-annotating') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
const allowFillingForms = (
|
||||||
|
document.getElementById('allow-filling-forms') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
const allowDocumentAssembly = (
|
||||||
|
document.getElementById('allow-document-assembly') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
const allowPageExtraction = (
|
||||||
|
document.getElementById('allow-page-extraction') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
|
||||||
|
if (finalOwnerPassword) {
|
||||||
|
if (!allowModifying) args.push('--modify=none');
|
||||||
|
if (!allowCopying) args.push('--extract=n');
|
||||||
|
if (!allowPrinting) args.push('--print=none');
|
||||||
|
if (!allowAnnotating) args.push('--annotate=n');
|
||||||
|
if (!allowDocumentAssembly) args.push('--assemble=n');
|
||||||
|
if (!allowFillingForms) args.push('--form=n');
|
||||||
|
if (!allowPageExtraction) args.push('--extract=n');
|
||||||
|
// --modify-other is not directly mapped, apply if modifying is disabled
|
||||||
|
if (!allowModifying) args.push('--modify-other=n');
|
||||||
|
} else if (finalUserPassword) {
|
||||||
|
args.push('--allow-insecure');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args.push('--decrypt');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent = 'Applying new permissions...';
|
args.push('--', outputPath);
|
||||||
|
try {
|
||||||
|
qpdf.callMain(args);
|
||||||
|
} catch (qpdfError: any) {
|
||||||
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
|
||||||
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement).checked;
|
const errorMsg = qpdfError.message || '';
|
||||||
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;
|
|
||||||
|
|
||||||
|
if (
|
||||||
const doc = new PDFDocument({
|
errorMsg.includes('invalid password') ||
|
||||||
size: [pageImages[0].width, pageImages[0].height],
|
errorMsg.includes('incorrect password') ||
|
||||||
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
|
errorMsg.includes('password')
|
||||||
|
) {
|
||||||
// Apply the new, separate user and owner passwords
|
throw new Error('INVALID_PASSWORD');
|
||||||
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();
|
if (
|
||||||
stream.on('finish', function () {
|
errorMsg.includes('encrypted') ||
|
||||||
const blob = stream.toBlob('application/pdf');
|
errorMsg.includes('password required')
|
||||||
|
) {
|
||||||
|
throw new Error('PASSWORD_REQUIRED');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Preparing download...');
|
||||||
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
|
||||||
|
if (!outputFile || outputFile.length === 0) {
|
||||||
|
throw new Error('Processing resulted in an empty file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
hexToRgb,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
@@ -17,7 +20,9 @@ async function updateTextColorPreview() {
|
|||||||
if (!textColorCanvas) return;
|
if (!textColorCanvas) return;
|
||||||
|
|
||||||
// @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(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const page = await pdf.getPage(1); // Preview first page
|
const page = await pdf.getPage(1); // Preview first page
|
||||||
const viewport = page.getViewport({ scale: 0.8 });
|
const viewport = page.getViewport({ scale: 0.8 });
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
||||||
@@ -29,17 +34,25 @@ async function updateTextColorPreview() {
|
|||||||
textColorCanvas.height = viewport.height;
|
textColorCanvas.height = viewport.height;
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
|
const imageData = context.getImageData(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
0,
|
||||||
const imageData = context.getImageData(0, 0, textColorCanvas.width, textColorCanvas.height);
|
0,
|
||||||
|
(textColorCanvas as HTMLCanvasElement).width,
|
||||||
|
(textColorCanvas as HTMLCanvasElement).height
|
||||||
|
);
|
||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const colorHex = (
|
||||||
const colorHex = document.getElementById('text-color-input').value;
|
document.getElementById('text-color-input') as HTMLInputElement
|
||||||
|
).value;
|
||||||
const { r, g, b } = hexToRgb(colorHex);
|
const { r, g, b } = hexToRgb(colorHex);
|
||||||
const darknessThreshold = 120;
|
const darknessThreshold = 120;
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
if (data[i] < darknessThreshold && data[i + 1] < darknessThreshold && data[i + 2] < darknessThreshold) {
|
if (
|
||||||
|
data[i] < darknessThreshold &&
|
||||||
|
data[i + 1] < darknessThreshold &&
|
||||||
|
data[i + 2] < darknessThreshold
|
||||||
|
) {
|
||||||
data[i] = r * 255;
|
data[i] = r * 255;
|
||||||
data[i + 1] = g * 255;
|
data[i + 1] = g * 255;
|
||||||
data[i + 2] = b * 255;
|
data[i + 2] = b * 255;
|
||||||
@@ -66,7 +79,9 @@ export async function setupTextColorTool() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// @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(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const page = await pdf.getPage(1);
|
const page = await pdf.getPage(1);
|
||||||
const viewport = page.getViewport({ scale: 0.8 });
|
const viewport = page.getViewport({ scale: 0.8 });
|
||||||
|
|
||||||
@@ -75,8 +90,10 @@ export async function setupTextColorTool() {
|
|||||||
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
||||||
originalCanvas.height = viewport.height;
|
originalCanvas.height = viewport.height;
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
await page.render({
|
||||||
await page.render({ canvasContext: originalCanvas.getContext('2d'), viewport }).promise;
|
canvasContext: (originalCanvas as HTMLCanvasElement).getContext('2d'),
|
||||||
|
viewport,
|
||||||
|
}).promise;
|
||||||
await updateTextColorPreview();
|
await updateTextColorPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +112,9 @@ export async function changeTextColor() {
|
|||||||
try {
|
try {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
// @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(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||||
@@ -113,7 +132,11 @@ export async function changeTextColor() {
|
|||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
|
|
||||||
for (let j = 0; j < data.length; j += 4) {
|
for (let j = 0; j < data.length; j += 4) {
|
||||||
if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) {
|
if (
|
||||||
|
data[j] < darknessThreshold &&
|
||||||
|
data[j + 1] < darknessThreshold &&
|
||||||
|
data[j + 2] < darknessThreshold
|
||||||
|
) {
|
||||||
data[j] = r * 255;
|
data[j] = r * 255;
|
||||||
data[j + 1] = g * 255;
|
data[j + 1] = g * 255;
|
||||||
data[j + 2] = b * 255;
|
data[j + 2] = b * 255;
|
||||||
@@ -121,20 +144,30 @@ export async function changeTextColor() {
|
|||||||
}
|
}
|
||||||
context.putImageData(imageData, 0, 0);
|
context.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
const pngImageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}, 'image/png'));
|
}, 'image/png')
|
||||||
|
);
|
||||||
|
|
||||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
newPage.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'text-color-changed.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'text-color-changed.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not change text color.');
|
showAlert('Error', 'Could not change text color.');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -53,7 +52,7 @@ export async function combineToSinglePage() {
|
|||||||
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
||||||
|
|
||||||
if (addSeparator && i < sourcePages.length - 1) {
|
if (addSeparator && i < sourcePages.length - 1) {
|
||||||
const lineY = currentY - (spacing / 2);
|
const lineY = currentY - spacing / 2;
|
||||||
newPage.drawLine({
|
newPage.drawLine({
|
||||||
start: { x: 0, y: lineY },
|
start: { x: 0, y: lineY },
|
||||||
end: { x: maxWidth, y: lineY },
|
end: { x: maxWidth, y: lineY },
|
||||||
@@ -66,8 +65,10 @@ export async function combineToSinglePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
const newPdfBytes = await newDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'combined-page.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'combined-page.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while combining pages.');
|
showAlert('Error', 'An error occurred while combining pages.');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { icons, createIcons } from "lucide";
|
import { icons, createIcons } from 'lucide';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
pdfDoc1: null,
|
pdfDoc1: null,
|
||||||
@@ -17,7 +17,12 @@ const state = {
|
|||||||
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
|
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
|
||||||
* @param {HTMLElement} container - The container to fit the canvas into.
|
* @param {HTMLElement} container - The container to fit the canvas into.
|
||||||
*/
|
*/
|
||||||
async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any) {
|
async function renderPage(
|
||||||
|
pdfDoc: any,
|
||||||
|
pageNum: any,
|
||||||
|
canvas: any,
|
||||||
|
container: any
|
||||||
|
) {
|
||||||
const page = await pdfDoc.getPage(pageNum);
|
const page = await pdfDoc.getPage(pageNum);
|
||||||
|
|
||||||
// Calculate scale to fit the container width.
|
// Calculate scale to fit the container width.
|
||||||
@@ -35,7 +40,6 @@ async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any
|
|||||||
}).promise;
|
}).promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function renderBothPages() {
|
async function renderBothPages() {
|
||||||
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
||||||
|
|
||||||
@@ -52,8 +56,18 @@ async function renderBothPages() {
|
|||||||
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
|
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
renderPage(state.pdfDoc1, Math.min(state.currentPage, state.pdfDoc1.numPages), canvas1, container1),
|
renderPage(
|
||||||
renderPage(state.pdfDoc2, Math.min(state.currentPage, state.pdfDoc2.numPages), canvas2, container2)
|
state.pdfDoc1,
|
||||||
|
Math.min(state.currentPage, state.pdfDoc1.numPages),
|
||||||
|
canvas1,
|
||||||
|
container1
|
||||||
|
),
|
||||||
|
renderPage(
|
||||||
|
state.pdfDoc2,
|
||||||
|
Math.min(state.currentPage, state.pdfDoc2.numPages),
|
||||||
|
canvas2,
|
||||||
|
container2
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
updateNavControls();
|
updateNavControls();
|
||||||
@@ -61,15 +75,21 @@ async function renderBothPages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateNavControls() {
|
function updateNavControls() {
|
||||||
const maxPages = Math.max(state.pdfDoc1?.numPages || 0, state.pdfDoc2?.numPages || 0);
|
const maxPages = Math.max(
|
||||||
|
state.pdfDoc1?.numPages || 0,
|
||||||
|
state.pdfDoc2?.numPages || 0
|
||||||
|
);
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
document.getElementById('current-page-display-compare').textContent = state.currentPage;
|
document.getElementById('current-page-display-compare').textContent =
|
||||||
|
state.currentPage;
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
document.getElementById('total-pages-display-compare').textContent = maxPages;
|
document.getElementById('total-pages-display-compare').textContent = maxPages;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('prev-page-compare').disabled = state.currentPage <= 1;
|
document.getElementById('prev-page-compare').disabled =
|
||||||
|
state.currentPage <= 1;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('next-page-compare').disabled = state.currentPage >= maxPages;
|
document.getElementById('next-page-compare').disabled =
|
||||||
|
state.currentPage >= maxPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
||||||
@@ -77,7 +97,8 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
|||||||
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
|
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
|
||||||
|
|
||||||
const handleFile = async (file: any) => {
|
const handleFile = async (file: any) => {
|
||||||
if (!file || file.type !== 'application/pdf') return showAlert('Invalid File', 'Please select a valid PDF file.');
|
if (!file || file.type !== 'application/pdf')
|
||||||
|
return showAlert('Invalid File', 'Please select a valid PDF file.');
|
||||||
|
|
||||||
const displayDiv = document.getElementById(displayId);
|
const displayDiv = document.getElementById(displayId);
|
||||||
displayDiv.textContent = '';
|
displayDiv.textContent = '';
|
||||||
@@ -110,7 +131,10 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
|||||||
await renderBothPages();
|
await renderBothPages();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showAlert('Error', 'Could not load PDF. It may be corrupt or password-protected.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not load PDF. It may be corrupt or password-protected.'
|
||||||
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -120,7 +144,10 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
|||||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||||
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
|
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
|
||||||
dropZone.addEventListener('dragover', (e) => e.preventDefault());
|
dropZone.addEventListener('dragover', (e) => e.preventDefault());
|
||||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); });
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleFile(e.dataTransfer.files[0]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,7 +164,6 @@ function setViewMode(mode: any) {
|
|||||||
const canvas2 = document.getElementById('canvas-compare-2');
|
const canvas2 = document.getElementById('canvas-compare-2');
|
||||||
const opacitySlider = document.getElementById('opacity-slider');
|
const opacitySlider = document.getElementById('opacity-slider');
|
||||||
|
|
||||||
|
|
||||||
if (mode === 'overlay') {
|
if (mode === 'overlay') {
|
||||||
wrapper.className = 'compare-viewer-wrapper overlay-mode';
|
wrapper.className = 'compare-viewer-wrapper overlay-mode';
|
||||||
overlayControls.classList.remove('hidden');
|
overlayControls.classList.remove('hidden');
|
||||||
@@ -163,20 +189,30 @@ export function setupCompareTool() {
|
|||||||
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
|
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
|
||||||
|
|
||||||
document.getElementById('prev-page-compare').addEventListener('click', () => {
|
document.getElementById('prev-page-compare').addEventListener('click', () => {
|
||||||
if (state.currentPage > 1) { state.currentPage--; renderBothPages(); }
|
if (state.currentPage > 1) {
|
||||||
|
state.currentPage--;
|
||||||
|
renderBothPages();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.getElementById('next-page-compare').addEventListener('click', () => {
|
document.getElementById('next-page-compare').addEventListener('click', () => {
|
||||||
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
||||||
if (state.currentPage < maxPages) { state.currentPage++; renderBothPages(); }
|
if (state.currentPage < maxPages) {
|
||||||
|
state.currentPage++;
|
||||||
|
renderBothPages();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('view-mode-overlay').addEventListener('click', () => setViewMode('overlay'));
|
document
|
||||||
document.getElementById('view-mode-side').addEventListener('click', () => setViewMode('side-by-side'));
|
.getElementById('view-mode-overlay')
|
||||||
|
.addEventListener('click', () => setViewMode('overlay'));
|
||||||
|
document
|
||||||
|
.getElementById('view-mode-side')
|
||||||
|
.addEventListener('click', () => setViewMode('side-by-side'));
|
||||||
|
|
||||||
const canvas2 = document.getElementById('canvas-compare-2');
|
const canvas2 = document.getElementById('canvas-compare-2');
|
||||||
document.getElementById('flicker-btn').addEventListener('click', () => {
|
document.getElementById('flicker-btn').addEventListener('click', () => {
|
||||||
canvas2.style.transition = 'opacity 150ms ease-in-out';
|
canvas2.style.transition = 'opacity 150ms ease-in-out';
|
||||||
canvas2.style.opacity = (canvas2.style.opacity === '0') ? '1' : '0';
|
canvas2.style.opacity = canvas2.style.opacity === '0' ? '1' : '0';
|
||||||
});
|
});
|
||||||
document.getElementById('opacity-slider').addEventListener('input', (e) => {
|
document.getElementById('opacity-slider').addEventListener('input', (e) => {
|
||||||
canvas2.style.transition = '';
|
canvas2.style.transition = '';
|
||||||
@@ -187,22 +223,23 @@ export function setupCompareTool() {
|
|||||||
const panel1 = document.getElementById('panel-1');
|
const panel1 = document.getElementById('panel-1');
|
||||||
const panel2 = document.getElementById('panel-2');
|
const panel2 = document.getElementById('panel-2');
|
||||||
const syncToggle = document.getElementById('sync-scroll-toggle');
|
const syncToggle = document.getElementById('sync-scroll-toggle');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
(syncToggle as HTMLInputElement).addEventListener('change', () => {
|
||||||
syncToggle.addEventListener('change', () => { state.isSyncScroll = syncToggle.checked; });
|
state.isSyncScroll = (syncToggle as HTMLInputElement).checked;
|
||||||
|
});
|
||||||
|
|
||||||
let scrollingPanel: any = null;
|
let scrollingPanel: any = null;
|
||||||
panel1.addEventListener('scroll', () => {
|
panel1.addEventListener('scroll', () => {
|
||||||
if (state.isSyncScroll && scrollingPanel !== panel2) {
|
if (state.isSyncScroll && scrollingPanel !== panel2) {
|
||||||
scrollingPanel = panel1;
|
scrollingPanel = panel1;
|
||||||
panel2.scrollTop = panel1.scrollTop;
|
panel2.scrollTop = panel1.scrollTop;
|
||||||
setTimeout(() => scrollingPanel = null, 100);
|
setTimeout(() => (scrollingPanel = null), 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
panel2.addEventListener('scroll', () => {
|
panel2.addEventListener('scroll', () => {
|
||||||
if (state.isSyncScroll && scrollingPanel !== panel1) {
|
if (state.isSyncScroll && scrollingPanel !== panel1) {
|
||||||
scrollingPanel = panel2;
|
scrollingPanel = panel2;
|
||||||
panel1.scrollTop = panel2.scrollTop;
|
panel1.scrollTop = panel2.scrollTop;
|
||||||
setTimeout(() => scrollingPanel = null, 100);
|
setTimeout(() => (scrollingPanel = null), 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+102
-37
@@ -1,9 +1,12 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
|
|
||||||
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
|
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
|
||||||
|
|
||||||
function dataUrlToBytes(dataUrl: any) {
|
function dataUrlToBytes(dataUrl: any) {
|
||||||
@@ -18,7 +21,9 @@ function dataUrlToBytes(dataUrl: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function performSmartCompression(arrayBuffer: any, settings: any) {
|
async function performSmartCompression(arrayBuffer: any, settings: any) {
|
||||||
const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
|
const pdfDoc = await PDFDocument.load(arrayBuffer, {
|
||||||
|
ignoreEncryption: true,
|
||||||
|
});
|
||||||
const pages = pdfDoc.getPages();
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
if (settings.removeMetadata) {
|
if (settings.removeMetadata) {
|
||||||
@@ -44,20 +49,29 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
|
|
||||||
for (const [key, value] of xobjects.entries()) {
|
for (const [key, value] of xobjects.entries()) {
|
||||||
const stream = pdfDoc.context.lookup(value);
|
const stream = pdfDoc.context.lookup(value);
|
||||||
if (!(stream instanceof PDFStream) || stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')) continue;
|
if (
|
||||||
|
!(stream instanceof PDFStream) ||
|
||||||
|
stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imageBytes = stream.getContents();
|
const imageBytes = stream.getContents();
|
||||||
if (imageBytes.length < settings.skipSize) continue;
|
if (imageBytes.length < settings.skipSize) continue;
|
||||||
|
|
||||||
const width = stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
|
const width =
|
||||||
|
stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
|
||||||
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
|
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
|
||||||
: 0;
|
: 0;
|
||||||
const height = stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
|
const height =
|
||||||
|
stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
|
||||||
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
|
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
|
||||||
: 0;
|
: 0;
|
||||||
const bitsPerComponent = stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
const bitsPerComponent =
|
||||||
? (stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber).asNumber()
|
stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
||||||
|
? (
|
||||||
|
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
|
||||||
|
).asNumber()
|
||||||
: 8;
|
: 8;
|
||||||
|
|
||||||
if (width > 0 && height > 0) {
|
if (width > 0 && height > 0) {
|
||||||
@@ -88,7 +102,9 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
canvas.height = Math.floor(newHeight);
|
canvas.height = Math.floor(newHeight);
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
const imageUrl = URL.createObjectURL(new Blob([new Uint8Array(imageBytes)]));
|
const imageUrl = URL.createObjectURL(
|
||||||
|
new Blob([new Uint8Array(imageBytes)])
|
||||||
|
);
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
img.onload = resolve;
|
img.onload = resolve;
|
||||||
@@ -119,13 +135,18 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
|
|
||||||
if (settings.tryWebP) {
|
if (settings.tryWebP) {
|
||||||
try {
|
try {
|
||||||
const webpDataUrl = canvas.toDataURL('image/webp', settings.quality);
|
const webpDataUrl = canvas.toDataURL(
|
||||||
|
'image/webp',
|
||||||
|
settings.quality
|
||||||
|
);
|
||||||
const webpBytes = dataUrlToBytes(webpDataUrl);
|
const webpBytes = dataUrlToBytes(webpDataUrl);
|
||||||
if (webpBytes.length < bestSize) {
|
if (webpBytes.length < bestSize) {
|
||||||
bestBytes = webpBytes;
|
bestBytes = webpBytes;
|
||||||
bestSize = webpBytes.length;
|
bestSize = webpBytes.length;
|
||||||
}
|
}
|
||||||
} catch (e) { /* WebP not supported */ }
|
} catch (e) {
|
||||||
|
/* WebP not supported */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
|
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
|
||||||
@@ -138,7 +159,10 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
|
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
|
||||||
|
|
||||||
if (settings.grayscale) {
|
if (settings.grayscale) {
|
||||||
stream.dict.set(PDFName.of('ColorSpace'), PDFName.of('DeviceGray'));
|
stream.dict.set(
|
||||||
|
PDFName.of('ColorSpace'),
|
||||||
|
PDFName.of('DeviceGray')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
@@ -152,7 +176,7 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
const saveOptions = {
|
const saveOptions = {
|
||||||
useObjectStreams: settings.useObjectStreams !== false,
|
useObjectStreams: settings.useObjectStreams !== false,
|
||||||
addDefaultPage: false,
|
addDefaultPage: false,
|
||||||
objectsPerTick: settings.objectsPerTick || 50
|
objectsPerTick: settings.objectsPerTick || 50,
|
||||||
};
|
};
|
||||||
|
|
||||||
return await pdfDoc.save(saveOptions);
|
return await pdfDoc.save(saveOptions);
|
||||||
@@ -175,14 +199,22 @@ async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
|||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport, canvas: canvas }).promise;
|
await page.render({ canvasContext: context, viewport, canvas: canvas })
|
||||||
|
.promise;
|
||||||
|
|
||||||
const jpegBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', settings.quality));
|
const jpegBlob = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob(resolve, 'image/jpeg', settings.quality)
|
||||||
|
);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||||
const jpegBytes = await jpegBlob.arrayBuffer();
|
const jpegBytes = await jpegBlob.arrayBuffer();
|
||||||
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
||||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
newPage.drawImage(jpegImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
newPage.drawImage(jpegImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return await newPdfDoc.save();
|
return await newPdfDoc.save();
|
||||||
}
|
}
|
||||||
@@ -194,22 +226,46 @@ export async function compress() {
|
|||||||
const algorithm = document.getElementById('compression-algorithm').value;
|
const algorithm = document.getElementById('compression-algorithm').value;
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
'balanced': {
|
balanced: {
|
||||||
smart: { quality: 0.5, threshold: 0.95, maxWidth: 1800, maxHeight: 1800, skipSize: 3000 },
|
smart: {
|
||||||
legacy: { scale: 1.5, quality: 0.6 }
|
quality: 0.5,
|
||||||
|
threshold: 0.95,
|
||||||
|
maxWidth: 1800,
|
||||||
|
maxHeight: 1800,
|
||||||
|
skipSize: 3000,
|
||||||
|
},
|
||||||
|
legacy: { scale: 1.5, quality: 0.6 },
|
||||||
},
|
},
|
||||||
'high-quality': {
|
'high-quality': {
|
||||||
smart: { quality: 0.70, threshold: 0.98, maxWidth: 2500, maxHeight: 2500, skipSize: 5000 },
|
smart: {
|
||||||
legacy: { scale: 2.0, quality: 0.9 }
|
quality: 0.7,
|
||||||
|
threshold: 0.98,
|
||||||
|
maxWidth: 2500,
|
||||||
|
maxHeight: 2500,
|
||||||
|
skipSize: 5000,
|
||||||
|
},
|
||||||
|
legacy: { scale: 2.0, quality: 0.9 },
|
||||||
},
|
},
|
||||||
'small-size': {
|
'small-size': {
|
||||||
smart: { quality: 0.3, threshold: 0.95, maxWidth: 1200, maxHeight: 1200, skipSize: 2000 },
|
smart: {
|
||||||
legacy: { scale: 1.2, quality: 0.4 }
|
quality: 0.3,
|
||||||
|
threshold: 0.95,
|
||||||
|
maxWidth: 1200,
|
||||||
|
maxHeight: 1200,
|
||||||
|
skipSize: 2000,
|
||||||
|
},
|
||||||
|
legacy: { scale: 1.2, quality: 0.4 },
|
||||||
|
},
|
||||||
|
extreme: {
|
||||||
|
smart: {
|
||||||
|
quality: 0.1,
|
||||||
|
threshold: 0.95,
|
||||||
|
maxWidth: 1000,
|
||||||
|
maxHeight: 1000,
|
||||||
|
skipSize: 1000,
|
||||||
|
},
|
||||||
|
legacy: { scale: 1.0, quality: 0.2 },
|
||||||
},
|
},
|
||||||
'extreme': {
|
|
||||||
smart: { quality: 0.1, threshold: 0.95, maxWidth: 1000, maxHeight: 1000, skipSize: 1000 },
|
|
||||||
legacy: { scale: 1.0, quality: 0.2 }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
||||||
@@ -232,16 +288,21 @@ export async function compress() {
|
|||||||
usedMethod = 'Photon';
|
usedMethod = 'Photon';
|
||||||
} else {
|
} else {
|
||||||
showLoader('Running Automatic (Vector first)...');
|
showLoader('Running Automatic (Vector first)...');
|
||||||
const vectorResultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
const vectorResultBytes = await performSmartCompression(
|
||||||
|
arrayBuffer,
|
||||||
|
smartSettings
|
||||||
|
);
|
||||||
|
|
||||||
if (vectorResultBytes.length < originalFile.size) {
|
if (vectorResultBytes.length < originalFile.size) {
|
||||||
resultBytes = vectorResultBytes;
|
resultBytes = vectorResultBytes;
|
||||||
usedMethod = 'Vector (Automatic)';
|
usedMethod = 'Vector (Automatic)';
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
showAlert('Vector failed to reduce size. Trying Photon...', 'info');
|
||||||
showAlert('Vector failed to reduce size. Trying Photon...', 'info', 3000);
|
|
||||||
showLoader('Running Automatic (Photon fallback)...');
|
showLoader('Running Automatic (Photon fallback)...');
|
||||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
resultBytes = await performLegacyCompression(
|
||||||
|
arrayBuffer,
|
||||||
|
legacySettings
|
||||||
|
);
|
||||||
usedMethod = 'Photon (Automatic)';
|
usedMethod = 'Photon (Automatic)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +310,8 @@ export async function compress() {
|
|||||||
const originalSize = formatBytes(originalFile.size);
|
const originalSize = formatBytes(originalFile.size);
|
||||||
const compressedSize = formatBytes(resultBytes.length);
|
const compressedSize = formatBytes(resultBytes.length);
|
||||||
const savings = originalFile.size - resultBytes.length;
|
const savings = originalFile.size - resultBytes.length;
|
||||||
const savingsPercent = savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
const savingsPercent =
|
||||||
|
savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
if (savings > 0) {
|
if (savings > 0) {
|
||||||
showAlert(
|
showAlert(
|
||||||
@@ -267,12 +329,15 @@ export async function compress() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(new Blob([resultBytes], { type: 'application/pdf' }), 'compressed-final.pdf');
|
downloadFile(
|
||||||
|
new Blob([resultBytes], { type: 'application/pdf' }),
|
||||||
|
'compressed-final.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
showAlert(
|
||||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
'Error',
|
||||||
showAlert('Error', `An error occurred during compression. Error: ${e.message}`, 'error');
|
`An error occurred during compression. Error: ${e.message}`
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-29
@@ -1,11 +1,10 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import Cropper from "cropperjs";
|
import Cropper from 'cropperjs';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
|
|
||||||
// --- Global State for the Cropper Tool ---
|
// --- Global State for the Cropper Tool ---
|
||||||
const cropperState = {
|
const cropperState = {
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
@@ -88,7 +87,7 @@ async function displayPageAsImage(num: any) {
|
|||||||
showAlert('Ready', 'Please select an area to crop.');
|
showAlert('Ready', 'Please select an area to crop.');
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error rendering page:", error);
|
console.error('Error rendering page:', error);
|
||||||
showAlert('Error', 'Failed to render page.');
|
showAlert('Error', 'Failed to render page.');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
@@ -110,14 +109,17 @@ async function changePage(offset: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updatePageInfo() {
|
function updatePageInfo() {
|
||||||
document.getElementById('page-info').textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
document.getElementById('page-info').textContent =
|
||||||
|
`Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableControls() {
|
function enableControls() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('prev-page').disabled = cropperState.currentPageNum <= 1;
|
document.getElementById('prev-page').disabled =
|
||||||
|
cropperState.currentPageNum <= 1;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('next-page').disabled = cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
|
document.getElementById('next-page').disabled =
|
||||||
|
cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('crop-button').disabled = false;
|
document.getElementById('crop-button').disabled = false;
|
||||||
}
|
}
|
||||||
@@ -176,7 +178,9 @@ async function performFlatteningCrop(cropData: any) {
|
|||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
||||||
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes);
|
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
||||||
|
cropperState.originalPdfBytes
|
||||||
|
);
|
||||||
const totalPages = cropperState.pdfDoc.numPages;
|
const totalPages = cropperState.pdfDoc.numPages;
|
||||||
|
|
||||||
for (let i = 0; i < totalPages; i++) {
|
for (let i = 0; i < totalPages; i++) {
|
||||||
@@ -203,24 +207,37 @@ async function performFlatteningCrop(cropData: any) {
|
|||||||
|
|
||||||
finalCtx.drawImage(
|
finalCtx.drawImage(
|
||||||
tempCanvas,
|
tempCanvas,
|
||||||
tempCanvas.width * crop.x, tempCanvas.height * crop.y,
|
tempCanvas.width * crop.x,
|
||||||
finalWidth, finalHeight,
|
tempCanvas.height * crop.y,
|
||||||
0, 0, finalWidth, finalHeight
|
finalWidth,
|
||||||
|
finalHeight,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
finalWidth,
|
||||||
|
finalHeight
|
||||||
);
|
);
|
||||||
|
|
||||||
const pngBytes = await new Promise(res => finalCanvas.toBlob(blob => blob.arrayBuffer().then(res), 'image/png'));
|
const pngBytes = await new Promise((res) =>
|
||||||
|
finalCanvas.toBlob((blob) => blob.arrayBuffer().then(res), 'image/png')
|
||||||
|
);
|
||||||
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||||
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
||||||
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight });
|
newPage.drawImage(embeddedImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: finalWidth,
|
||||||
|
height: finalHeight,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
|
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [
|
||||||
|
i,
|
||||||
|
]);
|
||||||
newPdfDoc.addPage(copiedPage);
|
newPdfDoc.addPage(copiedPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newPdfDoc;
|
return newPdfDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function setupCropperTool() {
|
export async function setupCropperTool() {
|
||||||
if (state.files.length === 0) return;
|
if (state.files.length === 0) return;
|
||||||
|
|
||||||
@@ -243,21 +260,30 @@ export async function setupCropperTool() {
|
|||||||
|
|
||||||
await displayPageAsImage(cropperState.currentPageNum);
|
await displayPageAsImage(cropperState.currentPageNum);
|
||||||
|
|
||||||
document.getElementById('prev-page').addEventListener('click', () => changePage(-1));
|
document
|
||||||
document.getElementById('next-page').addEventListener('click', () => changePage(1));
|
.getElementById('prev-page')
|
||||||
|
.addEventListener('click', () => changePage(-1));
|
||||||
|
document
|
||||||
|
.getElementById('next-page')
|
||||||
|
.addEventListener('click', () => changePage(1));
|
||||||
|
|
||||||
document.getElementById('crop-button').addEventListener('click', async () => {
|
document
|
||||||
|
.getElementById('crop-button')
|
||||||
|
.addEventListener('click', async () => {
|
||||||
// Get the last known crop from the active page before processing
|
// Get the last known crop from the active page before processing
|
||||||
saveCurrentCrop();
|
saveCurrentCrop();
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
const isDestructive = (
|
||||||
const isDestructive = document.getElementById('destructive-crop-toggle').checked;
|
document.getElementById('destructive-crop-toggle') as HTMLInputElement
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
).checked;
|
||||||
const isApplyToAll = document.getElementById('apply-to-all-toggle').checked;
|
const isApplyToAll = (
|
||||||
|
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
|
||||||
let finalCropData = {};
|
let finalCropData = {};
|
||||||
if (isApplyToAll) {
|
if (isApplyToAll) {
|
||||||
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
|
const currentCrop =
|
||||||
|
cropperState.pageCrops[cropperState.currentPageNum];
|
||||||
if (!currentCrop) {
|
if (!currentCrop) {
|
||||||
showAlert('No Crop Area', 'Please select an area to crop first.');
|
showAlert('No Crop Area', 'Please select an area to crop first.');
|
||||||
return;
|
return;
|
||||||
@@ -268,14 +294,20 @@ export async function setupCropperTool() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If not applying to all, only process pages with saved crops
|
// If not applying to all, only process pages with saved crops
|
||||||
finalCropData = Object.keys(cropperState.pageCrops).reduce((obj, key) => {
|
finalCropData = Object.keys(cropperState.pageCrops).reduce(
|
||||||
|
(obj, key) => {
|
||||||
obj[key] = cropperState.pageCrops[key];
|
obj[key] = cropperState.pageCrops[key];
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(finalCropData).length === 0) {
|
if (Object.keys(finalCropData).length === 0) {
|
||||||
showAlert('No Crop Area', 'Please select an area on at least one page to crop.');
|
showAlert(
|
||||||
|
'No Crop Area',
|
||||||
|
'Please select an area on at least one page to crop.'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,13 +319,20 @@ export async function setupCropperTool() {
|
|||||||
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
||||||
finalPdfBytes = await newPdfDoc.save();
|
finalPdfBytes = await newPdfDoc.save();
|
||||||
} else {
|
} else {
|
||||||
const pdfToModify = await PDFLibDocument.load(cropperState.originalPdfBytes);
|
const pdfToModify = await PDFLibDocument.load(
|
||||||
|
cropperState.originalPdfBytes
|
||||||
|
);
|
||||||
await performMetadataCrop(pdfToModify, finalCropData);
|
await performMetadataCrop(pdfToModify, finalCropData);
|
||||||
finalPdfBytes = await pdfToModify.save();
|
finalPdfBytes = await pdfToModify.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
|
const fileName = isDestructive
|
||||||
downloadFile(new Blob([finalPdfBytes], { type: 'application/pdf' }), fileName);
|
? 'flattened_crop.pdf'
|
||||||
|
: 'standard_crop.pdf';
|
||||||
|
downloadFile(
|
||||||
|
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
||||||
|
fileName
|
||||||
|
);
|
||||||
showAlert('Success', 'Crop complete! Your download has started.');
|
showAlert('Success', 'Crop complete! Your download has started.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -303,7 +342,7 @@ export async function setupCropperTool() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting up cropper tool:", error);
|
console.error('Error setting up cropper tool:', error);
|
||||||
showAlert('Error', 'Failed to load PDF for cropping.');
|
showAlert('Error', 'Failed to load PDF for cropping.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+84
-44
@@ -1,65 +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);
|
qpdf = await initializeQpdf();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
|
||||||
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 = `Processing page ${i} of ${numPages}...`;
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
const page = await pdf.getPage(i);
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
|
||||||
const canvas = document.createElement('canvas');
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
canvas.height = viewport.height;
|
showLoader('Decrypting PDF...');
|
||||||
canvas.width = viewport.width;
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
const args = [inputPath, '--password=' + password, '--decrypt', outputPath];
|
||||||
pageImages.push({
|
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
try {
|
||||||
width: viewport.width,
|
qpdf.callMain(args);
|
||||||
height: viewport.height
|
} catch (qpdfError: any) {
|
||||||
});
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
|
||||||
|
if (
|
||||||
|
qpdfError.message?.includes('invalid password') ||
|
||||||
|
qpdfError.message?.includes('password')
|
||||||
|
) {
|
||||||
|
throw new Error('INVALID_PASSWORD');
|
||||||
|
}
|
||||||
|
throw qpdfError;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent = 'Building unlocked PDF...';
|
showLoader('Preparing download...');
|
||||||
const doc = new PDFDocument({ size: [pageImages[0].width, pageImages[0].height] });
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
downloadFile(blob, `unlocked-${file.name}`);
|
downloadFile(blob, `unlocked-${file.name}`);
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Success', 'Decryption complete! Your download has started.');
|
showAlert(
|
||||||
});
|
'Success',
|
||||||
} catch (error) {
|
'PDF decrypted successfully! Your download has started.'
|
||||||
console.error("Error during PDF decryption:", error);
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,14 @@ export async function deletePages() {
|
|||||||
const trimmedRange = range.trim();
|
const trimmedRange = range.trim();
|
||||||
if (trimmedRange.includes('-')) {
|
if (trimmedRange.includes('-')) {
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
|
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
|
||||||
} else {
|
} else {
|
||||||
const pageNum = Number(trimmedRange);
|
const pageNum = Number(trimmedRange);
|
||||||
@@ -41,13 +48,19 @@ export async function deletePages() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const indicesToKeep = Array.from({ length: totalPages }, (_, i) => i).filter(index => !indicesToDelete.has(index));
|
const indicesToKeep = Array.from(
|
||||||
|
{ length: totalPages },
|
||||||
|
(_, i) => i
|
||||||
|
).filter((index) => !indicesToDelete.has(index));
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'deleted-pages.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'deleted-pages.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not delete pages.');
|
showAlert('Error', 'Could not delete pages.');
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
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 Sortable from 'sortablejs'
|
import Sortable from 'sortablejs';
|
||||||
import {icons, createIcons} from "lucide";
|
import { icons, createIcons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
const duplicateOrganizeState = {
|
const duplicateOrganizeState = {
|
||||||
sortableInstances: {}
|
sortableInstances: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function initializePageGridSortable() {
|
function initializePageGridSortable() {
|
||||||
@@ -32,7 +32,7 @@ function initializePageGridSortable() {
|
|||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: any) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,9 @@ function attachEventListeners(element: any) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Duplicate button listener
|
// Duplicate button listener
|
||||||
element.querySelector('.duplicate-btn').addEventListener('click', (e: any) => {
|
element
|
||||||
|
.querySelector('.duplicate-btn')
|
||||||
|
.addEventListener('click', (e: any) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const clone = element.cloneNode(true);
|
const clone = element.cloneNode(true);
|
||||||
element.after(clone);
|
element.after(clone);
|
||||||
@@ -68,7 +70,10 @@ function attachEventListeners(element: any) {
|
|||||||
renumberPages();
|
renumberPages();
|
||||||
initializePageGridSortable();
|
initializePageGridSortable();
|
||||||
} else {
|
} else {
|
||||||
showAlert('Cannot Delete', 'You cannot delete the last page of the document.');
|
showAlert(
|
||||||
|
'Cannot Delete',
|
||||||
|
'You cannot delete the last page of the document.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,15 +95,18 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
|
await page.render({ canvasContext: canvas.getContext('2d'), viewport })
|
||||||
|
.promise;
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
wrapper.className =
|
||||||
|
'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
wrapper.dataset.originalPageIndex = i - 1;
|
wrapper.dataset.originalPageIndex = i - 1;
|
||||||
|
|
||||||
const imgContainer = document.createElement('div');
|
const imgContainer = document.createElement('div');
|
||||||
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
imgContainer.className =
|
||||||
|
'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = canvas.toDataURL();
|
img.src = canvas.toDataURL();
|
||||||
@@ -106,14 +114,16 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
imgContainer.appendChild(img);
|
imgContainer.appendChild(img);
|
||||||
|
|
||||||
const pageNumberSpan = document.createElement('span');
|
const pageNumberSpan = document.createElement('span');
|
||||||
pageNumberSpan.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
pageNumberSpan.className =
|
||||||
|
'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||||
pageNumberSpan.textContent = i.toString();
|
pageNumberSpan.textContent = i.toString();
|
||||||
|
|
||||||
const controlsDiv = document.createElement('div');
|
const controlsDiv = document.createElement('div');
|
||||||
controlsDiv.className = 'flex items-center justify-center gap-4';
|
controlsDiv.className = 'flex items-center justify-center gap-4';
|
||||||
|
|
||||||
const duplicateBtn = document.createElement('button');
|
const duplicateBtn = document.createElement('button');
|
||||||
duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
duplicateBtn.className =
|
||||||
|
'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||||
duplicateBtn.title = 'Duplicate Page';
|
duplicateBtn.title = 'Duplicate Page';
|
||||||
const duplicateIcon = document.createElement('i');
|
const duplicateIcon = document.createElement('i');
|
||||||
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
||||||
@@ -121,7 +131,8 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
duplicateBtn.appendChild(duplicateIcon);
|
duplicateBtn.appendChild(duplicateIcon);
|
||||||
|
|
||||||
const deleteBtn = document.createElement('button');
|
const deleteBtn = document.createElement('button');
|
||||||
deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
deleteBtn.className =
|
||||||
|
'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||||
deleteBtn.title = 'Delete Page';
|
deleteBtn.title = 'Delete Page';
|
||||||
const deleteIcon = document.createElement('i');
|
const deleteIcon = document.createElement('i');
|
||||||
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
||||||
@@ -139,22 +150,25 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function processAndSave() {
|
export async function processAndSave() {
|
||||||
showLoader('Building new PDF...');
|
showLoader('Building new PDF...');
|
||||||
try {
|
try {
|
||||||
const grid = document.getElementById('page-grid');
|
const grid = document.getElementById('page-grid');
|
||||||
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
const finalIndices = Array.from(finalPageElements).map((el) =>
|
||||||
const finalIndices = Array.from(finalPageElements).map(el => parseInt(el.dataset.originalPageIndex));
|
parseInt((el as HTMLElement).dataset.originalPageIndex)
|
||||||
|
);
|
||||||
|
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
||||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'organized.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to save the new PDF.');
|
showAlert('Error', 'Failed to save the new PDF.');
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
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, PDFString } from "pdf-lib"
|
import { PDFName, PDFString } from 'pdf-lib';
|
||||||
|
|
||||||
|
|
||||||
export async function editMetadata() {
|
export async function editMetadata() {
|
||||||
showLoader('Updating metadata...');
|
showLoader('Updating metadata...');
|
||||||
@@ -20,7 +19,12 @@ export async function editMetadata() {
|
|||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const keywords = document.getElementById('meta-keywords').value;
|
const keywords = document.getElementById('meta-keywords').value;
|
||||||
state.pdfDoc.setKeywords(keywords.split(',').map((k: any) => k.trim()).filter(Boolean));
|
state.pdfDoc.setKeywords(
|
||||||
|
keywords
|
||||||
|
.split(',')
|
||||||
|
.map((k: any) => k.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const creationDate = document.getElementById('meta-creation-date').value;
|
const creationDate = document.getElementById('meta-creation-date').value;
|
||||||
@@ -37,9 +41,20 @@ export async function editMetadata() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const infoDict = state.pdfDoc.getInfoDict();
|
const infoDict = state.pdfDoc.getInfoDict();
|
||||||
const standardKeys = new Set(['Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', 'CreationDate', 'ModDate']);
|
const standardKeys = new Set([
|
||||||
|
'Title',
|
||||||
|
'Author',
|
||||||
|
'Subject',
|
||||||
|
'Keywords',
|
||||||
|
'Creator',
|
||||||
|
'Producer',
|
||||||
|
'CreationDate',
|
||||||
|
'ModDate',
|
||||||
|
]);
|
||||||
|
|
||||||
const allKeys = infoDict.keys().map((key: any) => key.asString().substring(1)); // Clean keys
|
const allKeys = infoDict
|
||||||
|
.keys()
|
||||||
|
.map((key: any) => key.asString().substring(1)); // Clean keys
|
||||||
|
|
||||||
allKeys.forEach((key: any) => {
|
allKeys.forEach((key: any) => {
|
||||||
if (!standardKeys.has(key)) {
|
if (!standardKeys.has(key)) {
|
||||||
@@ -62,11 +77,16 @@ export async function editMetadata() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-edited.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'metadata-edited.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not update metadata. Please check that date formats are correct.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-55
@@ -1,73 +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 = (document.getElementById('password-input') as HTMLInputElement).value;
|
const userPassword =
|
||||||
if (!password.trim()) {
|
(document.getElementById('user-password-input') as HTMLInputElement)
|
||||||
showAlert('Input Required', 'Please enter a password.');
|
?.value || '';
|
||||||
|
const ownerPasswordInput =
|
||||||
|
(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 = `Processing page ${i} of ${numPages}...`;
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
const page = await pdf.getPage(i);
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
|
||||||
const canvas = document.createElement('canvas');
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
canvas.height = viewport.height;
|
showLoader('Encrypting PDF with 256-bit AES...');
|
||||||
canvas.width = viewport.width;
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
|
||||||
pageImages.push({
|
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
// Only add restrictions if a distinct owner password was provided
|
||||||
width: viewport.width,
|
if (hasDistinctOwnerPassword) {
|
||||||
height: viewport.height
|
args.push(
|
||||||
});
|
'--modify=none',
|
||||||
|
'--extract=n',
|
||||||
|
'--print=none',
|
||||||
|
'--accessibility=n',
|
||||||
|
'--annotate=n',
|
||||||
|
'--assemble=n',
|
||||||
|
'--form=n',
|
||||||
|
'--modify-other=n'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent = 'Encrypting and building PDF...';
|
args.push('--', outputPath);
|
||||||
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);
|
||||||
|
} catch (qpdfError: any) {
|
||||||
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
throw new Error(
|
||||||
|
'Encryption failed: ' + (qpdfError.message || 'Unknown error')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Preparing download...');
|
||||||
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
|
||||||
|
if (!outputFile || outputFile.length === 0) {
|
||||||
|
throw new Error('Encryption resulted in an empty file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
downloadFile(blob, `encrypted-${file.name}`);
|
downloadFile(blob, `encrypted-${file.name}`);
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Success', 'Encryption complete! Your download has started.');
|
|
||||||
});
|
let successMessage = 'PDF encrypted successfully with 256-bit AES!';
|
||||||
} catch (error) {
|
if (!hasDistinctOwnerPassword) {
|
||||||
console.error("Error during PDF encryption:", error);
|
successMessage +=
|
||||||
|
' Note: Without a separate owner password, the PDF has no usage restrictions.';
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert('Success', successMessage);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error during PDF encryption:', error);
|
||||||
hideLoader();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,14 @@ export async function extractPages() {
|
|||||||
const trimmedRange = range.trim();
|
const trimmedRange = range.trim();
|
||||||
if (trimmedRange.includes('-')) {
|
if (trimmedRange.includes('-')) {
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
|
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
|
||||||
} else {
|
} else {
|
||||||
const pageNum = Number(trimmedRange);
|
const pageNum = Number(trimmedRange);
|
||||||
@@ -43,7 +50,9 @@ export async function extractPages() {
|
|||||||
|
|
||||||
for (const index of sortedIndices) {
|
for (const index of sortedIndices) {
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
|
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
|
||||||
|
index as number,
|
||||||
|
]);
|
||||||
newPdf.addPage(copiedPage);
|
newPdf.addPage(copiedPage);
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||||
|
|||||||
@@ -9,21 +9,30 @@ export function setupFixDimensionsUI() {
|
|||||||
const customSizeWrapper = document.getElementById('custom-size-wrapper');
|
const customSizeWrapper = document.getElementById('custom-size-wrapper');
|
||||||
if (targetSizeSelect && customSizeWrapper) {
|
if (targetSizeSelect && customSizeWrapper) {
|
||||||
targetSizeSelect.addEventListener('change', () => {
|
targetSizeSelect.addEventListener('change', () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
customSizeWrapper.classList.toggle(
|
||||||
customSizeWrapper.classList.toggle('hidden', targetSizeSelect.value !== 'Custom');
|
'hidden',
|
||||||
|
(targetSizeSelect as HTMLSelectElement).value !== 'Custom'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fixDimensions() {
|
export async function fixDimensions() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const targetSizeKey = (
|
||||||
const targetSizeKey = document.getElementById('target-size').value;
|
document.getElementById('target-size') as HTMLSelectElement
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
).value;
|
||||||
const orientation = document.getElementById('orientation').value;
|
const orientation = (
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
document.getElementById('orientation') as HTMLSelectElement
|
||||||
const scalingMode = document.querySelector('input[name="scaling-mode"]:checked').value;
|
).value;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const backgroundColor = hexToRgb(document.getElementById('background-color').value);
|
const scalingMode = (
|
||||||
|
document.querySelector(
|
||||||
|
'input[name="scaling-mode"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
const backgroundColor = hexToRgb(
|
||||||
|
(document.getElementById('background-color') as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
|
||||||
showLoader('Standardizing pages...');
|
showLoader('Standardizing pages...');
|
||||||
try {
|
try {
|
||||||
@@ -39,7 +48,8 @@ export async function fixDimensions() {
|
|||||||
if (units === 'in') {
|
if (units === 'in') {
|
||||||
targetWidth = width * 72;
|
targetWidth = width * 72;
|
||||||
targetHeight = height * 72;
|
targetHeight = height * 72;
|
||||||
} else { // mm
|
} else {
|
||||||
|
// mm
|
||||||
targetWidth = width * (72 / 25.4);
|
targetWidth = width * (72 / 25.4);
|
||||||
targetHeight = height * (72 / 25.4);
|
targetHeight = height * (72 / 25.4);
|
||||||
}
|
}
|
||||||
@@ -61,11 +71,20 @@ export async function fixDimensions() {
|
|||||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||||
|
|
||||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||||
newPage.drawRectangle({ x: 0, y: 0, width: targetWidth, height: targetHeight, color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b) });
|
newPage.drawRectangle({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: targetWidth,
|
||||||
|
height: targetHeight,
|
||||||
|
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||||
|
});
|
||||||
|
|
||||||
const scaleX = targetWidth / sourceWidth;
|
const scaleX = targetWidth / sourceWidth;
|
||||||
const scaleY = targetHeight / sourceHeight;
|
const scaleY = targetHeight / sourceHeight;
|
||||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
const scale =
|
||||||
|
scalingMode === 'fit'
|
||||||
|
? Math.min(scaleX, scaleY)
|
||||||
|
: Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
const scaledWidth = sourceWidth * scale;
|
const scaledWidth = sourceWidth * scale;
|
||||||
const scaledHeight = sourceHeight * scale;
|
const scaledHeight = sourceHeight * scale;
|
||||||
@@ -73,12 +92,19 @@ export async function fixDimensions() {
|
|||||||
const x = (targetWidth - scaledWidth) / 2;
|
const x = (targetWidth - scaledWidth) / 2;
|
||||||
const y = (targetHeight - scaledHeight) / 2;
|
const y = (targetHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
newPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
newPage.drawPage(embeddedPage, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: scaledWidth,
|
||||||
|
height: scaledHeight,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
const newPdfBytes = await newDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'standardized.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'standardized.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while standardizing pages.');
|
showAlert('Error', 'An error occurred while standardizing pages.');
|
||||||
|
|||||||
+14
-4
@@ -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,15 +14,20 @@ 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(new Blob([flattenedBytes], { type: 'application/pdf' }), 'flattened.pdf');
|
downloadFile(
|
||||||
|
new Blob([flattenedBytes], { type: 'application/pdf' }),
|
||||||
|
'flattened.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if (e.message.includes('getForm')) {
|
if (e.message.includes('getForm')) {
|
||||||
showAlert('No Form Found', 'This PDF does not contain any form fields to flatten.');
|
showAlert(
|
||||||
|
'No Form Found',
|
||||||
|
'This PDF does not contain any form fields to flatten.'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showAlert('Error', 'Could not flatten the PDF.');
|
showAlert('Error', 'Could not flatten the PDF.');
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-29
@@ -9,7 +9,7 @@ import {
|
|||||||
PDFDropdown,
|
PDFDropdown,
|
||||||
PDFButton,
|
PDFButton,
|
||||||
PDFSignature,
|
PDFSignature,
|
||||||
PDFOptionList
|
PDFOptionList,
|
||||||
} from 'pdf-lib';
|
} from 'pdf-lib';
|
||||||
|
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
@@ -55,9 +55,11 @@ async function renderPage() {
|
|||||||
canvas.style.transformOrigin = 'top left';
|
canvas.style.transformOrigin = 'top left';
|
||||||
canvas.style.transform = `scale(${formState.scale})`;
|
canvas.style.transform = `scale(${formState.scale})`;
|
||||||
|
|
||||||
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), { ignoreEncryption: true });
|
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), {
|
||||||
|
ignoreEncryption: true,
|
||||||
|
});
|
||||||
const form = tempPdfDoc.getForm();
|
const form = tempPdfDoc.getForm();
|
||||||
Object.keys(fieldValues).forEach(fieldName => {
|
Object.keys(fieldValues).forEach((fieldName) => {
|
||||||
try {
|
try {
|
||||||
const field = form.getField(fieldName);
|
const field = form.getField(fieldName);
|
||||||
if (!field) return;
|
if (!field) return;
|
||||||
@@ -76,7 +78,9 @@ async function renderPage() {
|
|||||||
field.select(fieldValues[fieldName]);
|
field.select(fieldValues[fieldName]);
|
||||||
} else if (field instanceof PDFOptionList) {
|
} else if (field instanceof PDFOptionList) {
|
||||||
if (Array.isArray(fieldValues[fieldName])) {
|
if (Array.isArray(fieldValues[fieldName])) {
|
||||||
(fieldValues[fieldName] as any[]).forEach(option => field.select(option));
|
(fieldValues[fieldName] as any[]).forEach((option) =>
|
||||||
|
field.select(option)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -85,12 +89,13 @@ async function renderPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tempPdfBytes = await tempPdfDoc.save();
|
const tempPdfBytes = await tempPdfDoc.save();
|
||||||
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes }).promise;
|
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes })
|
||||||
|
.promise;
|
||||||
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
||||||
|
|
||||||
await tempPage.render({
|
await tempPage.render({
|
||||||
canvasContext: context,
|
canvasContext: context,
|
||||||
viewport: viewport
|
viewport: viewport,
|
||||||
} as any).promise;
|
} as any).promise;
|
||||||
|
|
||||||
const currentPageDisplay = document.getElementById('current-page-display');
|
const currentPageDisplay = document.getElementById('current-page-display');
|
||||||
@@ -98,8 +103,10 @@ async function renderPage() {
|
|||||||
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
||||||
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
|
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
|
||||||
|
|
||||||
if (currentPageDisplay) currentPageDisplay.textContent = String(currentPageNum);
|
if (currentPageDisplay)
|
||||||
if (totalPagesDisplay) totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
|
currentPageDisplay.textContent = String(currentPageNum);
|
||||||
|
if (totalPagesDisplay)
|
||||||
|
totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
|
||||||
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
|
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
|
||||||
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
|
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
|
||||||
|
|
||||||
@@ -129,8 +136,8 @@ function handleFormChange(event: Event) {
|
|||||||
value = input.checked ? 'on' : 'off';
|
value = input.checked ? 'on' : 'off';
|
||||||
} else if (input instanceof HTMLSelectElement && input.multiple) {
|
} else if (input instanceof HTMLSelectElement && input.multiple) {
|
||||||
value = Array.from(input.options)
|
value = Array.from(input.options)
|
||||||
.filter(option => option.selected)
|
.filter((option) => option.selected)
|
||||||
.map(option => option.value);
|
.map((option) => option.value);
|
||||||
} else {
|
} else {
|
||||||
value = input.value;
|
value = input.value;
|
||||||
}
|
}
|
||||||
@@ -149,7 +156,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
const labelText = name.replace(/[_-]/g, ' ');
|
const labelText = name.replace(/[_-]/g, ' ');
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
|
wrapper.className =
|
||||||
|
'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
|
|
||||||
const label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
label.htmlFor = `field-${name}`;
|
label.htmlFor = `field-${name}`;
|
||||||
@@ -174,7 +182,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
input.id = `field-${name}`;
|
input.id = `field-${name}`;
|
||||||
input.name = name;
|
input.name = name;
|
||||||
input.value = fieldValues[name];
|
input.value = fieldValues[name];
|
||||||
input.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
input.className =
|
||||||
|
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||||
inputElement = input;
|
inputElement = input;
|
||||||
} else if (field instanceof PDFCheckBox) {
|
} else if (field instanceof PDFCheckBox) {
|
||||||
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
||||||
@@ -182,7 +191,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
input.type = 'checkbox';
|
input.type = 'checkbox';
|
||||||
input.id = `field-${name}`;
|
input.id = `field-${name}`;
|
||||||
input.name = name;
|
input.name = name;
|
||||||
input.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
input.className =
|
||||||
|
'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||||
input.checked = field.isChecked();
|
input.checked = field.isChecked();
|
||||||
inputElement = input;
|
inputElement = input;
|
||||||
} else if (field instanceof PDFRadioGroup) {
|
} else if (field instanceof PDFRadioGroup) {
|
||||||
@@ -197,7 +207,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
radio.type = 'radio';
|
radio.type = 'radio';
|
||||||
radio.name = name;
|
radio.name = name;
|
||||||
radio.value = opt;
|
radio.value = opt;
|
||||||
radio.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
radio.className =
|
||||||
|
'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||||
if (opt === field.getSelected()) radio.checked = true;
|
if (opt === field.getSelected()) radio.checked = true;
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
@@ -216,7 +227,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
const select = document.createElement('select');
|
const select = document.createElement('select');
|
||||||
select.id = `field-${name}`;
|
select.id = `field-${name}`;
|
||||||
select.name = name;
|
select.name = name;
|
||||||
select.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
select.className =
|
||||||
|
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||||
|
|
||||||
if (field instanceof PDFOptionList) {
|
if (field instanceof PDFOptionList) {
|
||||||
select.multiple = true;
|
select.multiple = true;
|
||||||
@@ -234,7 +246,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
inputElement = select;
|
inputElement = select;
|
||||||
} else {
|
} else {
|
||||||
const unsupportedDiv = document.createElement('div');
|
const unsupportedDiv = document.createElement('div');
|
||||||
unsupportedDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
unsupportedDiv.className =
|
||||||
|
'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.className = 'text-sm text-gray-400';
|
p.className = 'text-sm text-gray-400';
|
||||||
if (field instanceof PDFSignature) {
|
if (field instanceof PDFSignature) {
|
||||||
@@ -273,7 +286,8 @@ export async function setupFormFiller() {
|
|||||||
formContainer.textContent = '';
|
formContainer.textContent = '';
|
||||||
|
|
||||||
if (fields.length === 0) {
|
if (fields.length === 0) {
|
||||||
formContainer.innerHTML = '<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
formContainer.innerHTML =
|
||||||
|
'<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
||||||
processBtn.classList.add('hidden');
|
processBtn.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
fields.forEach((field: any) => {
|
fields.forEach((field: any) => {
|
||||||
@@ -283,7 +297,8 @@ export async function setupFormFiller() {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`Error processing field "${field.getName()}":`, e);
|
console.error(`Error processing field "${field.getName()}":`, e);
|
||||||
const errorDiv = document.createElement('div');
|
const errorDiv = document.createElement('div');
|
||||||
errorDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
errorDiv.className =
|
||||||
|
'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
// Sanitize error message display
|
// Sanitize error message display
|
||||||
const p1 = document.createElement('p');
|
const p1 = document.createElement('p');
|
||||||
p1.className = 'text-sm text-gray-500';
|
p1.className = 'text-sm text-gray-500';
|
||||||
@@ -311,19 +326,28 @@ export async function setupFormFiller() {
|
|||||||
const prevPageBtn = document.getElementById('prev-page');
|
const prevPageBtn = document.getElementById('prev-page');
|
||||||
const nextPageBtn = document.getElementById('next-page');
|
const nextPageBtn = document.getElementById('next-page');
|
||||||
|
|
||||||
if (zoomInBtn) zoomInBtn.addEventListener('click', () => setZoom(formState.scale + 0.25));
|
if (zoomInBtn)
|
||||||
if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => setZoom(Math.max(1, formState.scale - 0.25)));
|
zoomInBtn.addEventListener('click', () =>
|
||||||
if (prevPageBtn) prevPageBtn.addEventListener('click', () => changePage(-1));
|
setZoom(formState.scale + 0.25)
|
||||||
|
);
|
||||||
|
if (zoomOutBtn)
|
||||||
|
zoomOutBtn.addEventListener('click', () =>
|
||||||
|
setZoom(Math.max(1, formState.scale - 0.25))
|
||||||
|
);
|
||||||
|
if (prevPageBtn)
|
||||||
|
prevPageBtn.addEventListener('click', () => changePage(-1));
|
||||||
if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1));
|
if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1));
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
const formFillerOptions = document.getElementById('form-filler-options');
|
const formFillerOptions = document.getElementById('form-filler-options');
|
||||||
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Critical error setting up form filler:", e);
|
console.error('Critical error setting up form filler:', e);
|
||||||
showAlert('Error', 'Failed to read PDF form data. The file may be corrupt or not a valid form.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to read PDF form data. The file may be corrupt or not a valid form.'
|
||||||
|
);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,7 +357,7 @@ export async function processAndDownloadForm() {
|
|||||||
try {
|
try {
|
||||||
const form = state.pdfDoc.getForm();
|
const form = state.pdfDoc.getForm();
|
||||||
|
|
||||||
Object.keys(fieldValues).forEach(fieldName => {
|
Object.keys(fieldValues).forEach((fieldName) => {
|
||||||
try {
|
try {
|
||||||
const field = form.getField(fieldName);
|
const field = form.getField(fieldName);
|
||||||
const value = fieldValues[fieldName];
|
const value = fieldValues[fieldName];
|
||||||
@@ -352,19 +376,24 @@ export async function processAndDownloadForm() {
|
|||||||
field.select(value);
|
field.select(value);
|
||||||
} else if (field instanceof PDFOptionList) {
|
} else if (field instanceof PDFOptionList) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach(option => field.select(option));
|
value.forEach((option) => field.select(option));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error processing field "${fieldName}" during download:`, e);
|
console.error(
|
||||||
|
`Error processing field "${fieldName}" during download:`,
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'filled-form.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'filled-form.pdf'
|
||||||
|
);
|
||||||
|
|
||||||
showAlert('Success', 'Form has been filled and downloaded.');
|
showAlert('Success', 'Form has been filled and downloaded.');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to save the filled form.');
|
showAlert('Error', 'Failed to save the filled form.');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
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';
|
||||||
@@ -16,20 +15,33 @@ export async function heicToPdf() {
|
|||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const conversionResult = await heic2any({
|
const conversionResult = await heic2any({
|
||||||
blob: file,
|
blob: file,
|
||||||
toType: "image/png",
|
toType: 'image/png',
|
||||||
});
|
});
|
||||||
const pngBlob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult;
|
const pngBlob = Array.isArray(conversionResult)
|
||||||
|
? conversionResult[0]
|
||||||
|
: conversionResult;
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
const pngBytes = await pngBlob.arrayBuffer();
|
||||||
|
|
||||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
page.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: pngImage.width,
|
||||||
|
height: pngImage.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_heic.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_heic.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,12 @@ function sanitizeImageAsJpeg(imageBytes: any) {
|
|||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
canvas.toBlob(
|
canvas.toBlob(
|
||||||
async (jpegBlob) => {
|
async (jpegBlob) => {
|
||||||
if (!jpegBlob) return reject(new Error('Canvas to JPEG conversion failed.'));
|
if (!jpegBlob)
|
||||||
|
return reject(new Error('Canvas to JPEG conversion failed.'));
|
||||||
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
|
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
|
||||||
}, 'image/jpeg', 0.9
|
},
|
||||||
|
'image/jpeg',
|
||||||
|
0.9
|
||||||
);
|
);
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
};
|
};
|
||||||
@@ -56,12 +59,11 @@ function sanitizeImageAsPng(imageBytes: any) {
|
|||||||
canvas.height = img.naturalHeight;
|
canvas.height = img.naturalHeight;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
canvas.toBlob(
|
canvas.toBlob(async (pngBlob) => {
|
||||||
async (pngBlob) => {
|
if (!pngBlob)
|
||||||
if (!pngBlob) return reject(new Error('Canvas to PNG conversion failed.'));
|
return reject(new Error('Canvas to PNG conversion failed.'));
|
||||||
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
|
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
|
||||||
}, 'image/png'
|
}, 'image/png');
|
||||||
);
|
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
@@ -72,7 +74,6 @@ function sanitizeImageAsPng(imageBytes: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function imageToPdf() {
|
export async function imageToPdf() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one image file.');
|
showAlert('No Files', 'Please select at least one image file.');
|
||||||
@@ -84,7 +85,7 @@ export async function imageToPdf() {
|
|||||||
const imageList = document.getElementById('image-list');
|
const imageList = document.getElementById('image-list');
|
||||||
const sortedFiles = Array.from(imageList.children)
|
const sortedFiles = Array.from(imageList.children)
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
.map(li => state.files.find(f => f.name === li.dataset.fileName))
|
.map((li) => state.files.find((f) => f.name === li.dataset.fileName))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
for (const file of sortedFiles) {
|
for (const file of sortedFiles) {
|
||||||
@@ -95,7 +96,9 @@ export async function imageToPdf() {
|
|||||||
try {
|
try {
|
||||||
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
|
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`);
|
console.warn(
|
||||||
|
`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`
|
||||||
|
);
|
||||||
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
|
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
|
||||||
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||||
}
|
}
|
||||||
@@ -103,27 +106,41 @@ export async function imageToPdf() {
|
|||||||
try {
|
try {
|
||||||
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
|
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`);
|
console.warn(
|
||||||
|
`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`
|
||||||
|
);
|
||||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For WebP and other types, convert to PNG to preserve transparency
|
// For WebP and other types, convert to PNG to preserve transparency
|
||||||
console.warn(`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`);
|
console.warn(
|
||||||
|
`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`
|
||||||
|
);
|
||||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = pdfDoc.addPage([image.width, image.height]);
|
const page = pdfDoc.addPage([image.width, image.height]);
|
||||||
page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
page.drawImage(image, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pdfDoc.getPageCount() === 0) {
|
if (pdfDoc.getPageCount() === 0) {
|
||||||
throw new Error("No valid images could be processed. Please check your files.");
|
throw new Error(
|
||||||
|
'No valid images could be processed. Please check your files.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from-images.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from-images.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
||||||
|
|||||||
+36
-6
@@ -30,7 +30,7 @@ import { txtToPdf } from './txt-to-pdf.js';
|
|||||||
import { invertColors } from './invert-colors.js';
|
import { invertColors } from './invert-colors.js';
|
||||||
// import { viewMetadata } from './view-metadata.js';
|
// import { viewMetadata } from './view-metadata.js';
|
||||||
import { reversePages } from './reverse-pages.js';
|
import { reversePages } from './reverse-pages.js';
|
||||||
import { mdToPdf } from './md-to-pdf.js';
|
// import { mdToPdf } from './md-to-pdf.js';
|
||||||
import { svgToPdf } from './svg-to-pdf.js';
|
import { svgToPdf } from './svg-to-pdf.js';
|
||||||
import { bmpToPdf } from './bmp-to-pdf.js';
|
import { bmpToPdf } from './bmp-to-pdf.js';
|
||||||
import { heicToPdf } from './heic-to-pdf.js';
|
import { heicToPdf } from './heic-to-pdf.js';
|
||||||
@@ -49,16 +49,29 @@ import { setupCompareTool } from './compare-pdfs.js';
|
|||||||
import { setupOcrTool } from './ocr-pdf.js';
|
import { setupOcrTool } from './ocr-pdf.js';
|
||||||
import { wordToPdf } from './word-to-pdf.js';
|
import { wordToPdf } from './word-to-pdf.js';
|
||||||
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
|
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
|
||||||
import { removeAnnotations, setupRemoveAnnotationsTool } from './remove-annotations.js';
|
import {
|
||||||
|
removeAnnotations,
|
||||||
|
setupRemoveAnnotationsTool,
|
||||||
|
} from './remove-annotations.js';
|
||||||
import { setupCropperTool } from './cropper.js';
|
import { setupCropperTool } from './cropper.js';
|
||||||
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
|
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
|
||||||
import { posterize, setupPosterizeTool } from './posterize.js';
|
import { posterize, setupPosterizeTool } from './posterize.js';
|
||||||
|
import {
|
||||||
|
removeBlankPages,
|
||||||
|
setupRemoveBlankPagesTool,
|
||||||
|
} from './remove-blank-pages.js';
|
||||||
|
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
||||||
|
import { linearizePdf } from './linearize.js';
|
||||||
|
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
|
||||||
|
import { 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,
|
||||||
@@ -86,7 +99,7 @@ export const toolLogic = {
|
|||||||
'txt-to-pdf': txtToPdf,
|
'txt-to-pdf': txtToPdf,
|
||||||
'invert-colors': invertColors,
|
'invert-colors': invertColors,
|
||||||
'reverse-pages': reversePages,
|
'reverse-pages': reversePages,
|
||||||
'md-to-pdf': mdToPdf,
|
// 'md-to-pdf': mdToPdf,
|
||||||
'svg-to-pdf': svgToPdf,
|
'svg-to-pdf': svgToPdf,
|
||||||
'bmp-to-pdf': bmpToPdf,
|
'bmp-to-pdf': bmpToPdf,
|
||||||
'heic-to-pdf': heicToPdf,
|
'heic-to-pdf': heicToPdf,
|
||||||
@@ -105,8 +118,25 @@ export const toolLogic = {
|
|||||||
'ocr-pdf': { setup: setupOcrTool },
|
'ocr-pdf': { setup: setupOcrTool },
|
||||||
'word-to-pdf': wordToPdf,
|
'word-to-pdf': wordToPdf,
|
||||||
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
|
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
|
||||||
'remove-annotations': { process: removeAnnotations, setup: setupRemoveAnnotationsTool },
|
'remove-annotations': {
|
||||||
'cropper': { setup: setupCropperTool },
|
process: removeAnnotations,
|
||||||
|
setup: setupRemoveAnnotationsTool,
|
||||||
|
},
|
||||||
|
cropper: { setup: setupCropperTool },
|
||||||
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller },
|
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller },
|
||||||
'posterize': { process: posterize, setup: setupPosterizeTool },
|
posterize: { process: posterize, setup: setupPosterizeTool },
|
||||||
|
'remove-blank-pages': {
|
||||||
|
process: removeBlankPages,
|
||||||
|
setup: setupRemoveBlankPagesTool,
|
||||||
|
},
|
||||||
|
'alternate-merge': {
|
||||||
|
process: alternateMerge,
|
||||||
|
setup: setupAlternateMergeTool,
|
||||||
|
},
|
||||||
|
linearize: linearizePdf,
|
||||||
|
'add-attachments': {
|
||||||
|
process: addAttachments,
|
||||||
|
setup: setupAddAttachmentsTool,
|
||||||
|
},
|
||||||
|
'sanitize-pdf': sanitizePdf,
|
||||||
};
|
};
|
||||||
@@ -4,7 +4,10 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function invertColors() {
|
export async function invertColors() {
|
||||||
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
|
if (!state.pdfDoc) {
|
||||||
|
showAlert('Error', 'PDF not loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
showLoader('Inverting PDF colors...');
|
showLoader('Inverting PDF colors...');
|
||||||
try {
|
try {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
@@ -30,19 +33,29 @@ export async function invertColors() {
|
|||||||
}
|
}
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
const pngImageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}, 'image/png'));
|
}, 'image/png')
|
||||||
|
);
|
||||||
|
|
||||||
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||||
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
newPage.drawImage(image, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'inverted.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'inverted.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not invert PDF colors.');
|
showAlert('Error', 'Could not invert PDF colors.');
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ function sanitizeImageAsJpeg(imageBytes: any) {
|
|||||||
|
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
reject(new Error('The provided file could not be loaded as an image. It may be corrupted.'));
|
reject(
|
||||||
|
new Error(
|
||||||
|
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
img.src = imageUrl;
|
img.src = imageUrl;
|
||||||
@@ -63,13 +67,20 @@ export async function jpgToPdf() {
|
|||||||
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
|
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
|
||||||
showAlert(`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`);
|
showAlert(
|
||||||
|
`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
||||||
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error(`Failed to process ${file.name} after sanitization:`, fallbackError);
|
console.error(
|
||||||
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
|
`Failed to process ${file.name} after sanitization:`,
|
||||||
|
fallbackError
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Could not process "${file.name}". The file may be corrupted.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +94,10 @@ export async function jpgToPdf() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_jpgs.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_jpgs.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Conversion Error', e.message);
|
showAlert('Conversion Error', e.message);
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import createModule from '@neslinesli93/qpdf-wasm';
|
||||||
|
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||||
|
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
|
||||||
|
import { state } from '../state';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
let qpdfInstance: any = null;
|
||||||
|
|
||||||
|
async function initializeQpdf() {
|
||||||
|
if (qpdfInstance) {
|
||||||
|
return qpdfInstance;
|
||||||
|
}
|
||||||
|
showLoader('Initializing optimization engine...');
|
||||||
|
try {
|
||||||
|
qpdfInstance = await createModule({
|
||||||
|
locateFile: () => '/qpdf.wasm',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize qpdf-wasm:', error);
|
||||||
|
showAlert(
|
||||||
|
'Initialization Error',
|
||||||
|
'Could not load the optimization engine. Please refresh the page and try again.'
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
return qpdfInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function linearizePdf() {
|
||||||
|
// Check if there are files and at least one PDF
|
||||||
|
const pdfFiles = state.files.filter(
|
||||||
|
(file: File) => file.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
if (!pdfFiles || pdfFiles.length === 0) {
|
||||||
|
showAlert('No PDF Files', 'Please upload at least one PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Optimizing PDFs for web view (linearizing)...');
|
||||||
|
const zip = new JSZip(); // Create a JSZip instance
|
||||||
|
let qpdf: any;
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
qpdf = await initializeQpdf();
|
||||||
|
|
||||||
|
for (let i = 0; i < pdfFiles.length; i++) {
|
||||||
|
const file = pdfFiles[i];
|
||||||
|
const inputPath = `/input_${i}.pdf`;
|
||||||
|
const outputPath = `/output_${i}.pdf`;
|
||||||
|
|
||||||
|
showLoader(`Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
|
|
||||||
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
|
|
||||||
|
const args = [inputPath, '--linearize', outputPath];
|
||||||
|
|
||||||
|
qpdf.callMain(args);
|
||||||
|
|
||||||
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
if (!outputFile || outputFile.length === 0) {
|
||||||
|
console.error(
|
||||||
|
`Linearization resulted in an empty file for ${file.name}.`
|
||||||
|
);
|
||||||
|
throw new Error(`Processing failed for ${file.name}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.file(`linearized-${file.name}`, outputFile, { binary: true });
|
||||||
|
successCount++;
|
||||||
|
} catch (fileError: any) {
|
||||||
|
errorCount++;
|
||||||
|
console.error(`Failed to linearize ${file.name}:`, fileError);
|
||||||
|
// Optionally add an error marker/file to the zip? For now, we just skip.
|
||||||
|
} finally {
|
||||||
|
// Clean up WASM filesystem for this file
|
||||||
|
try {
|
||||||
|
if (qpdf?.FS) {
|
||||||
|
if (qpdf.FS.analyzePath(inputPath).exists) {
|
||||||
|
qpdf.FS.unlink(inputPath);
|
||||||
|
}
|
||||||
|
if (qpdf.FS.analyzePath(outputPath).exists) {
|
||||||
|
qpdf.FS.unlink(outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to cleanup WASM FS for ${file.name}:`,
|
||||||
|
cleanupError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount === 0) {
|
||||||
|
throw new Error('No PDF files could be linearized.');
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Generating ZIP file...');
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'linearized-pdfs.zip');
|
||||||
|
|
||||||
|
let alertMessage = `${successCount} PDF(s) linearized successfully.`;
|
||||||
|
if (errorCount > 0) {
|
||||||
|
alertMessage += ` ${errorCount} file(s) failed.`;
|
||||||
|
}
|
||||||
|
showAlert('Processing Complete', alertMessage);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Linearization process error:', error);
|
||||||
|
showAlert(
|
||||||
|
'Linearization Failed',
|
||||||
|
`An error occurred: ${error.message || 'Unknown error'}.`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
-77
@@ -1,85 +1,95 @@
|
|||||||
// note: this is a work in progress
|
// // note: this is a work in progress
|
||||||
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 html2canvas from 'html2canvas';
|
// import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
|
// export async function mdToPdf() {
|
||||||
|
// // @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||||
|
// if (
|
||||||
|
// typeof window.jspdf === 'undefined' ||
|
||||||
|
// typeof window.html2canvas === 'undefined'
|
||||||
|
// ) {
|
||||||
|
// showAlert(
|
||||||
|
// 'Libraries Not Ready',
|
||||||
|
// 'PDF generation libraries are loading. Please try again.'
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
// const markdownContent = document.getElementById('md-input').value.trim();
|
||||||
|
// if (!markdownContent) {
|
||||||
|
// showAlert('Input Required', 'Please enter some Markdown text.');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// showLoader('Generating High-Quality PDF...');
|
||||||
|
|
||||||
export async function mdToPdf() {
|
// try {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
// // @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
|
||||||
if (typeof window.jspdf === 'undefined' || typeof window.html2canvas === 'undefined') {
|
// const htmlContent = marked.parse(markdownContent);
|
||||||
showAlert('Libraries Not Ready', 'PDF generation libraries are loading. Please try again.');
|
// // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
return;
|
// const pageFormat = document.getElementById('page-format').value;
|
||||||
}
|
// // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// const orientation = document.getElementById('orientation').value;
|
||||||
const markdownContent = document.getElementById('md-input').value.trim();
|
// // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
if (!markdownContent) {
|
// const marginSize = document.getElementById('margin-size').value;
|
||||||
showAlert('Input Required', 'Please enter some Markdown text.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showLoader('Generating High-Quality PDF...');
|
|
||||||
|
|
||||||
try {
|
// const tempContainer = document.createElement('div');
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
|
// tempContainer.style.cssText =
|
||||||
const htmlContent = marked.parse(markdownContent);
|
// 'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// const styleSheet = document.createElement('style');
|
||||||
const pageFormat = document.getElementById('page-format').value;
|
// styleSheet.textContent = `
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// body { font-family: Helvetica, Arial, sans-serif; line-height: 1.6; font-size: 12px; }
|
||||||
const orientation = document.getElementById('orientation').value;
|
// h1, h2, h3 { margin: 20px 0 10px 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// h1 { font-size: 2em; } h2 { font-size: 1.5em; }
|
||||||
const marginSize = document.getElementById('margin-size').value;
|
// p, blockquote, ul, ol, pre, table { margin: 0 0 16px 0; }
|
||||||
|
// blockquote { padding: 0 1em; color: #6a737d; border-left: .25em solid #dfe2e5; }
|
||||||
|
// pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 6px; }
|
||||||
|
// code { font-family: 'Courier New', monospace; background-color: rgba(27,31,35,.05); border-radius: 3px; padding: .2em .4em; }
|
||||||
|
// table { width: 100%; border-collapse: collapse; } th, td { padding: 6px 13px; border: 1px solid #dfe2e5; }
|
||||||
|
// img { max-width: 100%; }
|
||||||
|
// `;
|
||||||
|
// tempContainer.appendChild(styleSheet);
|
||||||
|
// tempContainer.innerHTML += htmlContent;
|
||||||
|
// document.body.appendChild(tempContainer);
|
||||||
|
|
||||||
const tempContainer = document.createElement('div');
|
// const canvas = await html2canvas(tempContainer, {
|
||||||
tempContainer.style.cssText = 'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
|
// scale: 2,
|
||||||
const styleSheet = document.createElement('style');
|
// useCORS: true,
|
||||||
styleSheet.textContent = `
|
// });
|
||||||
body { font-family: Helvetica, Arial, sans-serif; line-height: 1.6; font-size: 12px; }
|
// document.body.removeChild(tempContainer);
|
||||||
h1, h2, h3 { margin: 20px 0 10px 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
|
|
||||||
h1 { font-size: 2em; } h2 { font-size: 1.5em; }
|
|
||||||
p, blockquote, ul, ol, pre, table { margin: 0 0 16px 0; }
|
|
||||||
blockquote { padding: 0 1em; color: #6a737d; border-left: .25em solid #dfe2e5; }
|
|
||||||
pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 6px; }
|
|
||||||
code { font-family: 'Courier New', monospace; background-color: rgba(27,31,35,.05); border-radius: 3px; padding: .2em .4em; }
|
|
||||||
table { width: 100%; border-collapse: collapse; } th, td { padding: 6px 13px; border: 1px solid #dfe2e5; }
|
|
||||||
img { max-width: 100%; }
|
|
||||||
`;
|
|
||||||
tempContainer.appendChild(styleSheet);
|
|
||||||
tempContainer.innerHTML += htmlContent;
|
|
||||||
document.body.appendChild(tempContainer);
|
|
||||||
|
|
||||||
const canvas = await html2canvas(tempContainer, { scale: 2, useCORS: true });
|
// // @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||||
document.body.removeChild(tempContainer);
|
// const { jsPDF } = window.jspdf;
|
||||||
|
// const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
|
||||||
|
// const pageFormats = { a4: [210, 297], letter: [216, 279] };
|
||||||
|
// const format = pageFormats[pageFormat];
|
||||||
|
// const [pageWidth, pageHeight] =
|
||||||
|
// orientation === 'landscape' ? [format[1], format[0]] : format;
|
||||||
|
// const margins = { narrow: 10, normal: 20, wide: 30 };
|
||||||
|
// const margin = margins[marginSize];
|
||||||
|
// const contentWidth = pageWidth - margin * 2;
|
||||||
|
// const contentHeight = pageHeight - margin * 2;
|
||||||
|
// const imgData = canvas.toDataURL('image/png');
|
||||||
|
// const imgHeight = (canvas.height * contentWidth) / canvas.width;
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
// let heightLeft = imgHeight;
|
||||||
const { jsPDF } = window.jspdf;
|
// let position = margin;
|
||||||
const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
|
// pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||||
const pageFormats = { 'a4': [210, 297], 'letter': [216, 279] };
|
// heightLeft -= contentHeight;
|
||||||
const format = pageFormats[pageFormat];
|
|
||||||
const [pageWidth, pageHeight] = orientation === 'landscape' ? [format[1], format[0]] : format;
|
|
||||||
const margins = { 'narrow': 10, 'normal': 20, 'wide': 30 };
|
|
||||||
const margin = margins[marginSize];
|
|
||||||
const contentWidth = pageWidth - (margin * 2);
|
|
||||||
const contentHeight = pageHeight - (margin * 2);
|
|
||||||
const imgData = canvas.toDataURL('image/png');
|
|
||||||
const imgHeight = (canvas.height * contentWidth) / canvas.width;
|
|
||||||
|
|
||||||
let heightLeft = imgHeight;
|
// while (heightLeft > 0) {
|
||||||
let position = margin;
|
// position = position - pageHeight;
|
||||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
// pdf.addPage();
|
||||||
heightLeft -= contentHeight;
|
// pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||||
|
// heightLeft -= contentHeight;
|
||||||
|
// }
|
||||||
|
|
||||||
while (heightLeft > 0) {
|
// const pdfBlob = pdf.output('blob');
|
||||||
position = position - pageHeight;
|
// downloadFile(pdfBlob, 'markdown-document.pdf');
|
||||||
pdf.addPage();
|
// } catch (error) {
|
||||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
// console.error('MD to PDF conversion error:', error);
|
||||||
heightLeft -= contentHeight;
|
// showAlert('Conversion Error', 'Failed to generate PDF.');
|
||||||
}
|
// } finally {
|
||||||
|
// hideLoader();
|
||||||
const pdfBlob = pdf.output('blob');
|
// }
|
||||||
downloadFile(pdfBlob, 'markdown-document.pdf');
|
// }
|
||||||
} catch (error) {
|
|
||||||
console.error('MD to PDF conversion error:', error);
|
|
||||||
showAlert('Conversion Error', 'Failed to generate PDF.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+55
-26
@@ -17,7 +17,7 @@ const mergeState = {
|
|||||||
sortableInstances: {},
|
sortableInstances: {},
|
||||||
isRendering: false,
|
isRendering: false,
|
||||||
cachedThumbnails: null,
|
cachedThumbnails: null,
|
||||||
lastFileHash: null
|
lastFileHash: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function parsePageRanges(rangeString: any, totalPages: any) {
|
function parsePageRanges(rangeString: any, totalPages: any) {
|
||||||
@@ -29,7 +29,14 @@ function parsePageRanges(rangeString: any, totalPages: any) {
|
|||||||
const trimmedRange = range.trim();
|
const trimmedRange = range.trim();
|
||||||
if (trimmedRange.includes('-')) {
|
if (trimmedRange.includes('-')) {
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
indices.add(i - 1);
|
indices.add(i - 1);
|
||||||
}
|
}
|
||||||
@@ -65,7 +72,7 @@ function initializeFileListSortable() {
|
|||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: any) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +97,14 @@ function initializePageThumbnailsSortable() {
|
|||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: any) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateFileHash() {
|
function generateFileHash() {
|
||||||
return (state.files as File[]).map(f => `${f.name}-${f.size}-${f.lastModified}`).join('|');
|
return (state.files as File[])
|
||||||
|
.map((f) => `${f.name}-${f.size}-${f.lastModified}`)
|
||||||
|
.join('|');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderPageMergeThumbnails() {
|
async function renderPageMergeThumbnails() {
|
||||||
@@ -138,7 +147,9 @@ async function renderPageMergeThumbnails() {
|
|||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||||
currentPageNumber++;
|
currentPageNumber++;
|
||||||
showLoader(`Rendering page previews: ${currentPageNumber}/${totalPages}`);
|
showLoader(
|
||||||
|
`Rendering page previews: ${currentPageNumber}/${totalPages}`
|
||||||
|
);
|
||||||
const page = await pdfjsDoc.getPage(i);
|
const page = await pdfjsDoc.getPage(i);
|
||||||
const viewport = page.getViewport({ scale: 0.3 });
|
const viewport = page.getViewport({ scale: 0.3 });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@@ -148,11 +159,12 @@ async function renderPageMergeThumbnails() {
|
|||||||
await page.render({
|
await page.render({
|
||||||
canvasContext: context,
|
canvasContext: context,
|
||||||
canvas: canvas,
|
canvas: canvas,
|
||||||
viewport
|
viewport,
|
||||||
}).promise;
|
}).promise;
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
wrapper.className =
|
||||||
|
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
||||||
wrapper.dataset.fileName = file.name;
|
wrapper.dataset.fileName = file.name;
|
||||||
wrapper.dataset.pageIndex = (i - 1).toString();
|
wrapper.dataset.pageIndex = (i - 1).toString();
|
||||||
|
|
||||||
@@ -164,13 +176,15 @@ async function renderPageMergeThumbnails() {
|
|||||||
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
||||||
|
|
||||||
const pageNumDiv = document.createElement('div');
|
const pageNumDiv = document.createElement('div');
|
||||||
pageNumDiv.className = 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
|
pageNumDiv.className =
|
||||||
|
'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
|
||||||
pageNumDiv.textContent = i.toString();
|
pageNumDiv.textContent = i.toString();
|
||||||
|
|
||||||
imgContainer.append(img, pageNumDiv);
|
imgContainer.append(img, pageNumDiv);
|
||||||
|
|
||||||
const fileNamePara = document.createElement('p');
|
const fileNamePara = document.createElement('p');
|
||||||
fileNamePara.className = 'text-xs text-gray-400 truncate w-full text-center';
|
fileNamePara.className =
|
||||||
|
'text-xs text-gray-400 truncate w-full text-center';
|
||||||
const fullTitle = `${file.name} (page ${i})`;
|
const fullTitle = `${file.name} (page ${i})`;
|
||||||
fileNamePara.title = fullTitle;
|
fileNamePara.title = fullTitle;
|
||||||
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
||||||
@@ -202,10 +216,12 @@ export async function merge() {
|
|||||||
|
|
||||||
if (mergeState.activeMode === 'file') {
|
if (mergeState.activeMode === 'file') {
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
const sortedFiles = Array.from(fileList.children).map(li => {
|
const sortedFiles = Array.from(fileList.children)
|
||||||
|
.map((li) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
return state.files.find(f => f.name === li.dataset.fileName);
|
return state.files.find((f) => f.name === li.dataset.fileName);
|
||||||
}).filter(Boolean);
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
for (const file of sortedFiles) {
|
for (const file of sortedFiles) {
|
||||||
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||||
@@ -220,11 +236,11 @@ export async function merge() {
|
|||||||
const totalPages = sourcePdf.getPageCount();
|
const totalPages = sourcePdf.getPageCount();
|
||||||
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
|
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
|
||||||
|
|
||||||
const indicesToCopy = pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
const indicesToCopy =
|
||||||
|
pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
||||||
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
||||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const pageContainer = document.getElementById('page-merge-preview');
|
const pageContainer = document.getElementById('page-merge-preview');
|
||||||
const pageElements = Array.from(pageContainer.children);
|
const pageElements = Array.from(pageContainer.children);
|
||||||
@@ -237,19 +253,26 @@ export async function merge() {
|
|||||||
|
|
||||||
const sourcePdf = mergeState.pdfDocs[fileName];
|
const sourcePdf = mergeState.pdfDocs[fileName];
|
||||||
if (sourcePdf && !isNaN(pageIndex)) {
|
if (sourcePdf && !isNaN(pageIndex)) {
|
||||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [pageIndex]);
|
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [
|
||||||
|
pageIndex,
|
||||||
|
]);
|
||||||
newPdfDoc.addPage(copiedPage);
|
newPdfDoc.addPage(copiedPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedPdfBytes = await newPdfDoc.save();
|
const mergedPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), 'merged.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'merged.pdf'
|
||||||
|
);
|
||||||
showAlert('Success', 'PDFs merged successfully!');
|
showAlert('Success', 'PDFs merged successfully!');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Merge error:', e);
|
console.error('Merge error:', e);
|
||||||
showAlert('Error', 'Failed to merge PDFs. Please check that all files are valid and not password-protected.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
@@ -267,9 +290,12 @@ export async function setupMergeTool() {
|
|||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
if (!mergeState.pdfDocs[file.name]) {
|
if (!mergeState.pdfDocs[file.name]) {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(
|
||||||
ignoreEncryption: true
|
pdfBytes as ArrayBuffer,
|
||||||
});
|
{
|
||||||
|
ignoreEncryption: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -287,13 +313,14 @@ export async function setupMergeTool() {
|
|||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
|
|
||||||
fileList.textContent = ''; // Clear list safely
|
fileList.textContent = ''; // Clear list safely
|
||||||
(state.files as File[]).forEach(f => {
|
(state.files as File[]).forEach((f) => {
|
||||||
const doc = mergeState.pdfDocs[f.name];
|
const doc = mergeState.pdfDocs[f.name];
|
||||||
const pageCount = doc ? doc.getPageCount() : 'N/A';
|
const pageCount = doc ? doc.getPageCount() : 'N/A';
|
||||||
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||||
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
li.className =
|
||||||
|
'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
||||||
li.dataset.fileName = f.name;
|
li.dataset.fileName = f.name;
|
||||||
|
|
||||||
const mainDiv = document.createElement('div');
|
const mainDiv = document.createElement('div');
|
||||||
@@ -305,7 +332,8 @@ export async function setupMergeTool() {
|
|||||||
nameSpan.textContent = f.name;
|
nameSpan.textContent = f.name;
|
||||||
|
|
||||||
const dragHandle = document.createElement('div');
|
const dragHandle = document.createElement('div');
|
||||||
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
|
dragHandle.className =
|
||||||
|
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
|
||||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
|
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
|
||||||
|
|
||||||
mainDiv.append(nameSpan, dragHandle);
|
mainDiv.append(nameSpan, dragHandle);
|
||||||
@@ -321,7 +349,8 @@ export async function setupMergeTool() {
|
|||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'text';
|
input.type = 'text';
|
||||||
input.id = `range-${safeFileName}`;
|
input.id = `range-${safeFileName}`;
|
||||||
input.className = 'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
input.className =
|
||||||
|
'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
||||||
input.placeholder = 'Leave blank for all pages';
|
input.placeholder = 'Leave blank for all pages';
|
||||||
|
|
||||||
rangeDiv.append(label, input);
|
rangeDiv.append(label, input);
|
||||||
|
|||||||
+28
-11
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -45,7 +44,10 @@ export async function nUpTool() {
|
|||||||
const firstPage = sourcePages[0];
|
const firstPage = sourcePages[0];
|
||||||
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
||||||
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
|
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
|
||||||
orientation = (isSourceLandscape && gridDims[0] > gridDims[1]) ? 'landscape' : 'portrait';
|
orientation =
|
||||||
|
isSourceLandscape && gridDims[0] > gridDims[1]
|
||||||
|
? 'landscape'
|
||||||
|
: 'portrait';
|
||||||
}
|
}
|
||||||
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
||||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||||
@@ -54,8 +56,8 @@ export async function nUpTool() {
|
|||||||
const margin = useMargins ? 36 : 0;
|
const margin = useMargins ? 36 : 0;
|
||||||
const gutter = useMargins ? 10 : 0;
|
const gutter = useMargins ? 10 : 0;
|
||||||
|
|
||||||
const usableWidth = pageWidth - (margin * 2);
|
const usableWidth = pageWidth - margin * 2;
|
||||||
const usableHeight = pageHeight - (margin * 2);
|
const usableHeight = pageHeight - margin * 2;
|
||||||
|
|
||||||
// Loop through the source pages in chunks of 'n'
|
// Loop through the source pages in chunks of 'n'
|
||||||
for (let i = 0; i < sourcePages.length; i += n) {
|
for (let i = 0; i < sourcePages.length; i += n) {
|
||||||
@@ -64,15 +66,20 @@ export async function nUpTool() {
|
|||||||
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
||||||
|
|
||||||
// Calculate dimensions of each cell in the grid
|
// Calculate dimensions of each cell in the grid
|
||||||
const cellWidth = (usableWidth - (gutter * (gridDims[0] - 1))) / gridDims[0];
|
const cellWidth =
|
||||||
const cellHeight = (usableHeight - (gutter * (gridDims[1] - 1))) / gridDims[1];
|
(usableWidth - gutter * (gridDims[0] - 1)) / gridDims[0];
|
||||||
|
const cellHeight =
|
||||||
|
(usableHeight - gutter * (gridDims[1] - 1)) / gridDims[1];
|
||||||
|
|
||||||
for (let j = 0; j < chunk.length; j++) {
|
for (let j = 0; j < chunk.length; j++) {
|
||||||
const sourcePage = chunk[j];
|
const sourcePage = chunk[j];
|
||||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||||
|
|
||||||
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
||||||
const scale = Math.min(cellWidth / embeddedPage.width, cellHeight / embeddedPage.height);
|
const scale = Math.min(
|
||||||
|
cellWidth / embeddedPage.width,
|
||||||
|
cellHeight / embeddedPage.height
|
||||||
|
);
|
||||||
const scaledWidth = embeddedPage.width * scale;
|
const scaledWidth = embeddedPage.width * scale;
|
||||||
const scaledHeight = embeddedPage.height * scale;
|
const scaledHeight = embeddedPage.height * scale;
|
||||||
|
|
||||||
@@ -80,17 +87,24 @@ export async function nUpTool() {
|
|||||||
const row = Math.floor(j / gridDims[0]);
|
const row = Math.floor(j / gridDims[0]);
|
||||||
const col = j % gridDims[0];
|
const col = j % gridDims[0];
|
||||||
const cellX = margin + col * (cellWidth + gutter);
|
const cellX = margin + col * (cellWidth + gutter);
|
||||||
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
const cellY =
|
||||||
|
pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
||||||
|
|
||||||
// Center the page within its cell
|
// Center the page within its cell
|
||||||
const x = cellX + (cellWidth - scaledWidth) / 2;
|
const x = cellX + (cellWidth - scaledWidth) / 2;
|
||||||
const y = cellY + (cellHeight - scaledHeight) / 2;
|
const y = cellY + (cellHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
outputPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
outputPage.drawPage(embeddedPage, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: scaledWidth,
|
||||||
|
height: scaledHeight,
|
||||||
|
});
|
||||||
|
|
||||||
if (addBorder) {
|
if (addBorder) {
|
||||||
outputPage.drawRectangle({
|
outputPage.drawRectangle({
|
||||||
x, y,
|
x,
|
||||||
|
y,
|
||||||
width: scaledWidth,
|
width: scaledWidth,
|
||||||
height: scaledHeight,
|
height: scaledHeight,
|
||||||
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
||||||
@@ -101,7 +115,10 @@ export async function nUpTool() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
const newPdfBytes = await newDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), `n-up_${n}.pdf`);
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
`n-up_${n}.pdf`
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
||||||
|
|||||||
+109
-39
@@ -4,14 +4,14 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import Tesseract from 'tesseract.js';
|
import Tesseract from 'tesseract.js';
|
||||||
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||||
import { icons, createIcons } from "lucide";
|
import { icons, createIcons } from 'lucide';
|
||||||
|
|
||||||
let searchablePdfBytes: any = null;
|
let searchablePdfBytes: any = null;
|
||||||
|
|
||||||
|
|
||||||
function sanitizeTextForWinAnsi(text: string): string {
|
function sanitizeTextForWinAnsi(text: string): string {
|
||||||
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
|
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
|
||||||
return text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
|
return text
|
||||||
|
.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
|
||||||
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ function parseHOCR(hocrText: string) {
|
|||||||
x0: parseInt(bboxMatch[1]),
|
x0: parseInt(bboxMatch[1]),
|
||||||
y0: parseInt(bboxMatch[2]),
|
y0: parseInt(bboxMatch[2]),
|
||||||
x1: parseInt(bboxMatch[3]),
|
x1: parseInt(bboxMatch[3]),
|
||||||
y1: parseInt(bboxMatch[4])
|
y1: parseInt(bboxMatch[4]),
|
||||||
},
|
},
|
||||||
confidence: confMatch ? parseInt(confMatch[1]) : 0
|
confidence: confMatch ? parseInt(confMatch[1]) : 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -56,7 +56,8 @@ function binarizeCanvas(ctx: any) {
|
|||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
// A simple luminance-based threshold for determining black or white
|
// A simple luminance-based threshold for determining black or white
|
||||||
const brightness = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
const brightness =
|
||||||
|
0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||||||
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
||||||
data[i] = data[i + 1] = data[i + 2] = color;
|
data[i] = data[i + 1] = data[i + 2] = color;
|
||||||
}
|
}
|
||||||
@@ -79,10 +80,10 @@ function updateProgress(status: any, progress: any) {
|
|||||||
progressLog.scrollTop = progressLog.scrollHeight;
|
progressLog.scrollTop = progressLog.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function runOCR() {
|
async function runOCR() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
const selectedLangs = Array.from(
|
||||||
const selectedLangs = Array.from(document.querySelectorAll('.lang-checkbox:checked')).map(cb => cb.value);
|
document.querySelectorAll('.lang-checkbox:checked')
|
||||||
|
).map((cb) => (cb as HTMLInputElement).value);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const scale = parseFloat(document.getElementById('ocr-resolution').value);
|
const scale = parseFloat(document.getElementById('ocr-resolution').value);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||||
@@ -91,7 +92,10 @@ async function runOCR() {
|
|||||||
const whitelist = document.getElementById('ocr-whitelist').value;
|
const whitelist = document.getElementById('ocr-whitelist').value;
|
||||||
|
|
||||||
if (selectedLangs.length === 0) {
|
if (selectedLangs.length === 0) {
|
||||||
showAlert('No Languages Selected', 'Please select at least one language for OCR.');
|
showAlert(
|
||||||
|
'No Languages Selected',
|
||||||
|
'Please select at least one language for OCR.'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const langString = selectedLangs.join('+');
|
const langString = selectedLangs.join('+');
|
||||||
@@ -101,7 +105,7 @@ async function runOCR() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const worker = await Tesseract.createWorker(langString, 1, {
|
const worker = await Tesseract.createWorker(langString, 1, {
|
||||||
logger: (m: any) => updateProgress(m.status, m.progress || 0)
|
logger: (m: any) => updateProgress(m.status, m.progress || 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable hOCR output
|
// Enable hOCR output
|
||||||
@@ -109,20 +113,23 @@ async function runOCR() {
|
|||||||
tessjs_create_hocr: '1',
|
tessjs_create_hocr: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (whitelist.trim()) {
|
|
||||||
await worker.setParameters({
|
await worker.setParameters({
|
||||||
tessedit_char_whitelist: whitelist.trim(),
|
tessedit_char_whitelist: whitelist,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// @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(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
let fullText = '';
|
let fullText = '';
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
updateProgress(`Processing page ${i} of ${pdf.numPages}`, (i - 1) / pdf.numPages);
|
updateProgress(
|
||||||
|
`Processing page ${i} of ${pdf.numPages}`,
|
||||||
|
(i - 1) / pdf.numPages
|
||||||
|
);
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const viewport = page.getViewport({ scale });
|
const viewport = page.getViewport({ scale });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@@ -135,17 +142,28 @@ async function runOCR() {
|
|||||||
binarizeCanvas(context);
|
binarizeCanvas(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await worker.recognize(canvas);
|
const result = await worker.recognize(
|
||||||
|
canvas,
|
||||||
|
{},
|
||||||
|
{ text: true, hocr: true }
|
||||||
|
);
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
const pngImageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}, 'image/png'));
|
}, 'image/png')
|
||||||
|
);
|
||||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
newPage.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
|
|
||||||
// Parse hOCR to get word-level data
|
// Parse hOCR to get word-level data
|
||||||
if (data.hocr) {
|
if (data.hocr) {
|
||||||
@@ -198,8 +216,13 @@ async function runOCR() {
|
|||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
document.getElementById('ocr-text-output').value = fullText.trim();
|
document.getElementById('ocr-text-output').value = fullText.trim();
|
||||||
|
|
||||||
document.getElementById('download-searchable-pdf').addEventListener('click', () => {
|
document
|
||||||
downloadFile(new Blob([searchablePdfBytes], { type: 'application/pdf' }), 'searchable.pdf');
|
.getElementById('download-searchable-pdf')
|
||||||
|
.addEventListener('click', () => {
|
||||||
|
downloadFile(
|
||||||
|
new Blob([searchablePdfBytes], { type: 'application/pdf' }),
|
||||||
|
'searchable.pdf'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// CHANGE: The copy button logic is updated to be safer.
|
// CHANGE: The copy button logic is updated to be safer.
|
||||||
@@ -230,49 +253,96 @@ async function runOCR() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('download-txt-btn').addEventListener('click', () => {
|
document
|
||||||
|
.getElementById('download-txt-btn')
|
||||||
|
.addEventListener('click', () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const textToSave = document.getElementById('ocr-text-output').value;
|
const textToSave = document.getElementById('ocr-text-output').value;
|
||||||
const blob = new Blob([textToSave], { type: 'text/plain' });
|
const blob = new Blob([textToSave], { type: 'text/plain' });
|
||||||
downloadFile(blob, 'ocr-text.txt');
|
downloadFile(blob, 'ocr-text.txt');
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('OCR Error', 'An error occurred during the OCR process. The worker may have failed to load. Please try again.');
|
showAlert(
|
||||||
|
'OCR Error',
|
||||||
|
'An error occurred during the OCR process. The worker may have failed to load. Please try again.'
|
||||||
|
);
|
||||||
document.getElementById('ocr-options').classList.remove('hidden');
|
document.getElementById('ocr-options').classList.remove('hidden');
|
||||||
document.getElementById('ocr-progress').classList.add('hidden');
|
document.getElementById('ocr-progress').classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the UI and event listeners for the OCR tool.
|
* Sets up the UI and event listeners for the OCR tool.
|
||||||
*/
|
*/
|
||||||
export function setupOcrTool() {
|
export function setupOcrTool() {
|
||||||
const langSearch = document.getElementById('lang-search');
|
const langSearch = document.getElementById('lang-search');
|
||||||
const langList = document.getElementById('lang-list');
|
const langList = document.getElementById('lang-list');
|
||||||
const selectedLangsDisplay = document.getElementById('selected-langs-display');
|
const selectedLangsDisplay = document.getElementById(
|
||||||
|
'selected-langs-display'
|
||||||
|
);
|
||||||
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) ? '' : 'none';
|
? ''
|
||||||
|
: 'none';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the display of selected languages
|
|
||||||
langList.addEventListener('change', () => {
|
langList.addEventListener('change', () => {
|
||||||
const selected = Array.from(langList.querySelectorAll('.lang-checkbox:checked'))
|
const selected = Array.from(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
langList.querySelectorAll('.lang-checkbox:checked')
|
||||||
.map(cb => tesseractLanguages[cb.value]);
|
).map((cb) => tesseractLanguages[(cb as HTMLInputElement).value]);
|
||||||
selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None';
|
selectedLangsDisplay.textContent =
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
selected.length > 0 ? selected.join(', ') : 'None';
|
||||||
processBtn.disabled = selected.length === 0;
|
(processBtn as HTMLButtonElement).disabled = selected.length === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attach the main OCR function to the process button
|
|
||||||
processBtn.addEventListener('click', runOCR);
|
processBtn.addEventListener('click', runOCR);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
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';
|
||||||
@@ -10,14 +9,18 @@ export async function organize() {
|
|||||||
try {
|
try {
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const pageContainer = document.getElementById('page-organizer');
|
const pageContainer = document.getElementById('page-organizer');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
const pageIndices = Array.from(pageContainer.children).map((child) =>
|
||||||
const pageIndices = Array.from(pageContainer.children).map(child => parseInt(child.dataset.pageIndex));
|
parseInt((child as HTMLElement).dataset.pageIndex)
|
||||||
|
);
|
||||||
|
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'organized.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not save the changes.');
|
showAlert('Error', 'Could not save the changes.');
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function renderTable(unit: any) {
|
|||||||
|
|
||||||
tableBody.textContent = ''; // Clear the table body safely
|
tableBody.textContent = ''; // Clear the table body safely
|
||||||
|
|
||||||
analyzedPagesData.forEach(pageData => {
|
analyzedPagesData.forEach((pageData) => {
|
||||||
const width = convertPoints(pageData.width, unit);
|
const width = convertPoints(pageData.width, unit);
|
||||||
const height = convertPoints(pageData.height, unit);
|
const height = convertPoints(pageData.height, unit);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function encodeBMP(imageData: any) {
|
|||||||
const view = new DataView(buffer);
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
// BMP File Header (14 bytes)
|
// BMP File Header (14 bytes)
|
||||||
view.setUint16(0, 0x4D42, true); // 'BM'
|
view.setUint16(0, 0x4d42, true); // 'BM'
|
||||||
view.setUint32(2, fileSize, true);
|
view.setUint32(2, fileSize, true);
|
||||||
view.setUint32(10, 54, true); // Offset to pixel data
|
view.setUint32(10, 54, true); // Offset to pixel data
|
||||||
|
|
||||||
@@ -43,19 +43,20 @@ function encodeBMP(imageData: any) {
|
|||||||
view.setUint8(offset++, data[i]); // Red
|
view.setUint8(offset++, data[i]); // Red
|
||||||
}
|
}
|
||||||
// Add padding to make the row a multiple of 4 bytes
|
// Add padding to make the row a multiple of 4 bytes
|
||||||
for (let p = 0; p < (stride - width * 3); p++) {
|
for (let p = 0; p < stride - width * 3; p++) {
|
||||||
view.setUint8(offset++, 0);
|
view.setUint8(offset++, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function pdfToBmp() {
|
export async function pdfToBmp() {
|
||||||
showLoader('Converting PDF to BMP images...');
|
showLoader('Converting PDF to BMP images...');
|
||||||
try {
|
try {
|
||||||
// @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(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
@@ -81,12 +82,14 @@ export async function pdfToBmp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Compressing files into a ZIP...');
|
showLoader('Compressing files into a ZIP...');
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_bmp_images.zip');
|
downloadFile(zipBlob, 'converted_bmp_images.zip');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert PDF to BMP. The file might be corrupted.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert PDF to BMP. The file might be corrupted.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,19 +36,29 @@ export async function pdfToGreyscale() {
|
|||||||
}
|
}
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
const imageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
const imageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}, 'image/png'));
|
}, 'image/png')
|
||||||
|
);
|
||||||
|
|
||||||
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
|
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
|
||||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||||
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
newPage.drawImage(image, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'greyscale.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'greyscale.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not convert to greyscale.');
|
showAlert('Error', 'Could not convert to greyscale.');
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ export async function pdfToJpg() {
|
|||||||
showLoader('Converting to JPG...');
|
showLoader('Converting to JPG...');
|
||||||
try {
|
try {
|
||||||
// @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(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
@@ -20,15 +22,20 @@ export async function pdfToJpg() {
|
|||||||
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
|
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9));
|
const blob = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob(resolve, 'image/jpeg', 0.9)
|
||||||
|
);
|
||||||
zip.file(`page_${i}.jpg`, blob as Blob);
|
zip.file(`page_${i}.jpg`, blob as Blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_images.zip');
|
downloadFile(zipBlob, 'converted_images.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert PDF to JPG. The file might be corrupted.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert PDF to JPG. The file might be corrupted.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -24,7 +23,10 @@ export async function pdfToMarkdown() {
|
|||||||
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
|
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Conversion Error', 'Failed to convert PDF. It may be image-based or corrupted.');
|
showAlert(
|
||||||
|
'Conversion Error',
|
||||||
|
'Failed to convert PDF. It may be image-based or corrupted.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
|
||||||
export async function pdfToPng() {
|
export async function pdfToPng() {
|
||||||
showLoader('Converting to PNG...');
|
showLoader('Converting to PNG...');
|
||||||
try {
|
try {
|
||||||
// @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(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
@@ -19,10 +19,12 @@ export async function pdfToPng() {
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
const blob = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob(resolve, 'image/png')
|
||||||
|
);
|
||||||
zip.file(`page_${i}.png`, blob as Blob);
|
zip.file(`page_${i}.png`, blob as Blob);
|
||||||
}
|
}
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_pngs.zip');
|
downloadFile(zipBlob, 'converted_pngs.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import UTIF from 'utif';
|
import UTIF from 'utif';
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
export async function pdfToTiff() {
|
export async function pdfToTiff() {
|
||||||
showLoader('Converting PDF to TIFF...');
|
showLoader('Converting PDF to TIFF...');
|
||||||
try {
|
try {
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
@@ -19,19 +21,30 @@ export async function pdfToTiff() {
|
|||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
await page.render({
|
||||||
|
canvasContext: context,
|
||||||
|
viewport: viewport,
|
||||||
|
canvas: canvas,
|
||||||
|
}).promise;
|
||||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
const rgba = imageData.data;
|
const rgba = imageData.data;
|
||||||
const tiffBuffer = UTIF.encodeImage(new Uint8Array(rgba), canvas.width, canvas.height);
|
const tiffBuffer = UTIF.encodeImage(
|
||||||
|
new Uint8Array(rgba),
|
||||||
|
canvas.width,
|
||||||
|
canvas.height
|
||||||
|
);
|
||||||
|
|
||||||
zip.file(`page_${i}.tiff`, tiffBuffer);
|
zip.file(`page_${i}.tiff`, tiffBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_tiff_images.zip');
|
downloadFile(zipBlob, 'converted_tiff_images.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert PDF to TIFF. The file might be corrupted.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert PDF to TIFF. The file might be corrupted.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -8,7 +7,9 @@ export async function pdfToWebp() {
|
|||||||
showLoader('Converting to WebP...');
|
showLoader('Converting to WebP...');
|
||||||
try {
|
try {
|
||||||
// @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(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
@@ -18,10 +19,12 @@ export async function pdfToWebp() {
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 0.9));
|
const blob = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob(resolve, 'image/webp', 0.9)
|
||||||
|
);
|
||||||
zip.file(`page_${i}.webp`, blob as Blob);
|
zip.file(`page_${i}.webp`, blob as Blob);
|
||||||
}
|
}
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_webp.zip');
|
downloadFile(zipBlob, 'converted_webp.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
|
||||||
export async function pdfToZip() {
|
export async function pdfToZip() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select one or more PDF files.');
|
showAlert('No Files', 'Please select one or more PDF files.');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -25,10 +24,16 @@ export async function pngToPdf() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_pngs.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_pngs.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to create PDF from PNG images. Ensure all files are valid PNGs.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to create PDF from PNG images. Ensure all files are valid PNGs.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-35
@@ -17,7 +17,9 @@ async function renderPosterizePreview(pageNum: number) {
|
|||||||
posterizeState.currentPage = pageNum;
|
posterizeState.currentPage = pageNum;
|
||||||
showLoader(`Rendering preview for page ${pageNum}...`);
|
showLoader(`Rendering preview for page ${pageNum}...`);
|
||||||
|
|
||||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
const canvas = document.getElementById(
|
||||||
|
'posterize-preview-canvas'
|
||||||
|
) as HTMLCanvasElement;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
if (posterizeState.pageSnapshots[pageNum]) {
|
if (posterizeState.pageSnapshots[pageNum]) {
|
||||||
@@ -30,7 +32,12 @@ async function renderPosterizePreview(pageNum: number) {
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
posterizeState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height);
|
posterizeState.pageSnapshots[pageNum] = context.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
canvas.width,
|
||||||
|
canvas.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePreviewNav();
|
updatePreviewNav();
|
||||||
@@ -41,16 +48,33 @@ async function renderPosterizePreview(pageNum: number) {
|
|||||||
function drawGridOverlay() {
|
function drawGridOverlay() {
|
||||||
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
|
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
|
||||||
|
|
||||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
const canvas = document.getElementById(
|
||||||
|
'posterize-preview-canvas'
|
||||||
|
) as HTMLCanvasElement;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
context.putImageData(posterizeState.pageSnapshots[posterizeState.currentPage], 0, 0);
|
context.putImageData(
|
||||||
|
posterizeState.pageSnapshots[posterizeState.currentPage],
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
const pageRangeInput = (
|
||||||
const pagesToProcess = parsePageRanges(pageRangeInput, posterizeState.pdfJsDoc.numPages);
|
document.getElementById('page-range') as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
const pagesToProcess = parsePageRanges(
|
||||||
|
pageRangeInput,
|
||||||
|
posterizeState.pdfJsDoc.numPages
|
||||||
|
);
|
||||||
|
|
||||||
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
|
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
|
||||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
const rows =
|
||||||
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
parseInt(
|
||||||
|
(document.getElementById('posterize-rows') as HTMLInputElement).value
|
||||||
|
) || 1;
|
||||||
|
const cols =
|
||||||
|
parseInt(
|
||||||
|
(document.getElementById('posterize-cols') as HTMLInputElement).value
|
||||||
|
) || 1;
|
||||||
|
|
||||||
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
||||||
context.lineWidth = 2;
|
context.lineWidth = 2;
|
||||||
@@ -78,29 +102,40 @@ function drawGridOverlay() {
|
|||||||
|
|
||||||
function updatePreviewNav() {
|
function updatePreviewNav() {
|
||||||
const currentPageSpan = document.getElementById('current-preview-page');
|
const currentPageSpan = document.getElementById('current-preview-page');
|
||||||
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement;
|
const prevBtn = document.getElementById(
|
||||||
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement;
|
'prev-preview-page'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const nextBtn = document.getElementById(
|
||||||
|
'next-preview-page'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
currentPageSpan.textContent = posterizeState.currentPage.toString();
|
currentPageSpan.textContent = posterizeState.currentPage.toString();
|
||||||
prevBtn.disabled = posterizeState.currentPage <= 1;
|
prevBtn.disabled = posterizeState.currentPage <= 1;
|
||||||
nextBtn.disabled = posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
|
nextBtn.disabled =
|
||||||
|
posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupPosterizeTool() {
|
export async function setupPosterizeTool() {
|
||||||
if (state.pdfDoc) {
|
if (state.pdfDoc) {
|
||||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
|
document.getElementById('total-pages').textContent = state.pdfDoc
|
||||||
|
.getPageCount()
|
||||||
|
.toString();
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes })
|
||||||
|
.promise;
|
||||||
posterizeState.pageSnapshots = {};
|
posterizeState.pageSnapshots = {};
|
||||||
posterizeState.currentPage = 1;
|
posterizeState.currentPage = 1;
|
||||||
|
|
||||||
document.getElementById('total-preview-pages').textContent = posterizeState.pdfJsDoc.numPages.toString();
|
document.getElementById('total-preview-pages').textContent =
|
||||||
|
posterizeState.pdfJsDoc.numPages.toString();
|
||||||
await renderPosterizePreview(1);
|
await renderPosterizePreview(1);
|
||||||
|
|
||||||
document.getElementById('prev-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage - 1);
|
document.getElementById('prev-preview-page').onclick = () =>
|
||||||
document.getElementById('next-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage + 1);
|
renderPosterizePreview(posterizeState.currentPage - 1);
|
||||||
|
document.getElementById('next-preview-page').onclick = () =>
|
||||||
|
renderPosterizePreview(posterizeState.currentPage + 1);
|
||||||
|
|
||||||
['posterize-rows', 'posterize-cols', 'page-range'].forEach(id => {
|
['posterize-rows', 'posterize-cols', 'page-range'].forEach((id) => {
|
||||||
document.getElementById(id).addEventListener('input', drawGridOverlay);
|
document.getElementById(id).addEventListener('input', drawGridOverlay);
|
||||||
});
|
});
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
@@ -110,14 +145,35 @@ export async function setupPosterizeTool() {
|
|||||||
export async function posterize() {
|
export async function posterize() {
|
||||||
showLoader('Posterizing PDF...');
|
showLoader('Posterizing PDF...');
|
||||||
try {
|
try {
|
||||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
const rows =
|
||||||
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
parseInt(
|
||||||
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value;
|
(document.getElementById('posterize-rows') as HTMLInputElement).value
|
||||||
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
|
) || 1;
|
||||||
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
|
const cols =
|
||||||
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0;
|
parseInt(
|
||||||
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value;
|
(document.getElementById('posterize-cols') as HTMLInputElement).value
|
||||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
) || 1;
|
||||||
|
const pageSizeKey = (
|
||||||
|
document.getElementById('output-page-size') as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
let orientation = (
|
||||||
|
document.getElementById('output-orientation') as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
const scalingMode = (
|
||||||
|
document.querySelector(
|
||||||
|
'input[name="scaling-mode"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
const overlap =
|
||||||
|
parseFloat(
|
||||||
|
(document.getElementById('overlap') as HTMLInputElement).value
|
||||||
|
) || 0;
|
||||||
|
const overlapUnits = (
|
||||||
|
document.getElementById('overlap-units') as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
const pageRangeInput = (
|
||||||
|
document.getElementById('page-range') as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
|
||||||
let overlapInPoints = overlap;
|
let overlapInPoints = overlap;
|
||||||
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
||||||
@@ -128,7 +184,7 @@ export async function posterize() {
|
|||||||
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||||
|
|
||||||
if (pageIndicesToProcess.length === 0) {
|
if (pageIndicesToProcess.length === 0) {
|
||||||
throw new Error("Invalid page range specified.");
|
throw new Error('Invalid page range specified.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
@@ -145,12 +201,16 @@ export async function posterize() {
|
|||||||
let currentOrientation = orientation;
|
let currentOrientation = orientation;
|
||||||
|
|
||||||
if (currentOrientation === 'auto') {
|
if (currentOrientation === 'auto') {
|
||||||
currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait';
|
currentOrientation =
|
||||||
|
viewport.width > viewport.height ? 'landscape' : 'portrait';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||||
} else if (currentOrientation === 'portrait' && targetWidth > targetHeight) {
|
} else if (
|
||||||
|
currentOrientation === 'portrait' &&
|
||||||
|
targetWidth > targetHeight
|
||||||
|
) {
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,20 +221,43 @@ export async function posterize() {
|
|||||||
for (let c = 0; c < cols; c++) {
|
for (let c = 0; c < cols; c++) {
|
||||||
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
||||||
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
||||||
const sWidth = tileWidth + (c > 0 ? overlapInPoints : 0) + (c < cols - 1 ? overlapInPoints : 0);
|
const sWidth =
|
||||||
const sHeight = tileHeight + (r > 0 ? overlapInPoints : 0) + (r < rows - 1 ? overlapInPoints : 0);
|
tileWidth +
|
||||||
|
(c > 0 ? overlapInPoints : 0) +
|
||||||
|
(c < cols - 1 ? overlapInPoints : 0);
|
||||||
|
const sHeight =
|
||||||
|
tileHeight +
|
||||||
|
(r > 0 ? overlapInPoints : 0) +
|
||||||
|
(r < rows - 1 ? overlapInPoints : 0);
|
||||||
|
|
||||||
const tileCanvas = document.createElement('canvas');
|
const tileCanvas = document.createElement('canvas');
|
||||||
tileCanvas.width = sWidth;
|
tileCanvas.width = sWidth;
|
||||||
tileCanvas.height = sHeight;
|
tileCanvas.height = sHeight;
|
||||||
tileCanvas.getContext('2d').drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
tileCanvas
|
||||||
|
.getContext('2d')
|
||||||
|
.drawImage(
|
||||||
|
tempCanvas,
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
sWidth,
|
||||||
|
sHeight,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
sWidth,
|
||||||
|
sHeight
|
||||||
|
);
|
||||||
|
|
||||||
const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png'));
|
const tileImage = await newDoc.embedPng(
|
||||||
|
tileCanvas.toDataURL('image/png')
|
||||||
|
);
|
||||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||||
|
|
||||||
const scaleX = newPage.getWidth() / sWidth;
|
const scaleX = newPage.getWidth() / sWidth;
|
||||||
const scaleY = newPage.getHeight() / sHeight;
|
const scaleY = newPage.getHeight() / sHeight;
|
||||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
const scale =
|
||||||
|
scalingMode === 'fit'
|
||||||
|
? Math.min(scaleX, scaleY)
|
||||||
|
: Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
const scaledWidth = sWidth * scale;
|
const scaledWidth = sWidth * scale;
|
||||||
const scaledHeight = sHeight * scale;
|
const scaledHeight = sHeight * scale;
|
||||||
@@ -190,9 +273,11 @@ export async function posterize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
const newPdfBytes = await newDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'posterized.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'posterized.pdf'
|
||||||
|
);
|
||||||
showAlert('Success', 'Your PDF has been posterized.');
|
showAlert('Success', 'Your PDF has been posterized.');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Could not posterize the PDF.');
|
showAlert('Error', e.message || 'Could not posterize the PDF.');
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export async function redact(redactions: any, canvasScale: any) {
|
|||||||
const pdfX = r.canvasX * conversionScale;
|
const pdfX = r.canvasX * conversionScale;
|
||||||
const pdfWidth = r.canvasWidth * conversionScale;
|
const pdfWidth = r.canvasWidth * conversionScale;
|
||||||
const pdfHeight = r.canvasHeight * conversionScale;
|
const pdfHeight = r.canvasHeight * conversionScale;
|
||||||
const pdfY = pageHeight - (r.canvasY * conversionScale) - pdfHeight;
|
const pdfY = pageHeight - r.canvasY * conversionScale - pdfHeight;
|
||||||
|
|
||||||
page.drawRectangle({
|
page.drawRectangle({
|
||||||
x: pdfX,
|
x: pdfX,
|
||||||
@@ -31,7 +31,10 @@ export async function redact(redactions: any, canvasScale: any) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const redactedBytes = await state.pdfDoc.save();
|
const redactedBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([redactedBytes], { type: 'application/pdf' }), 'redacted.pdf');
|
downloadFile(
|
||||||
|
new Blob([redactedBytes], { type: 'application/pdf' }),
|
||||||
|
'redacted.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to apply redactions.');
|
showAlert('Error', 'Failed to apply redactions.');
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { PDFName } from 'pdf-lib';
|
|||||||
|
|
||||||
export function setupRemoveAnnotationsTool() {
|
export function setupRemoveAnnotationsTool() {
|
||||||
if (state.pdfDoc) {
|
if (state.pdfDoc) {
|
||||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount();
|
document.getElementById('total-pages').textContent =
|
||||||
|
state.pdfDoc.getPageCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
|
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
|
||||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||||
pageScopeRadios.forEach(radio => {
|
pageScopeRadios.forEach((radio) => {
|
||||||
radio.addEventListener('change', () => {
|
radio.addEventListener('change', () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||||
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
|
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
|
||||||
@@ -20,21 +21,66 @@ export function setupRemoveAnnotationsTool() {
|
|||||||
const selectAllCheckbox = document.getElementById('select-all-annotations');
|
const selectAllCheckbox = document.getElementById('select-all-annotations');
|
||||||
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
|
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
|
||||||
selectAllCheckbox.addEventListener('change', () => {
|
selectAllCheckbox.addEventListener('change', () => {
|
||||||
allAnnotCheckboxes.forEach(checkbox => {
|
allAnnotCheckboxes.forEach((checkbox) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
checkbox.checked = selectAllCheckbox.checked;
|
checkbox.checked = selectAllCheckbox.checked;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
let targetPageIndices = [];
|
let targetPageIndices = [];
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
const pageScope = (
|
||||||
const pageScope = document.querySelector('input[name="page-scope"]:checked').value;
|
document.querySelector(
|
||||||
|
'input[name="page-scope"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
if (pageScope === 'all') {
|
if (pageScope === 'all') {
|
||||||
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
} else {
|
} else {
|
||||||
@@ -46,7 +92,14 @@ export async function removeAnnotations() {
|
|||||||
const trimmedRange = range.trim();
|
const trimmedRange = range.trim();
|
||||||
if (trimmedRange.includes('-')) {
|
if (trimmedRange.includes('-')) {
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
|
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
|
||||||
} else {
|
} else {
|
||||||
const pageNum = Number(trimmedRange);
|
const pageNum = Number(trimmedRange);
|
||||||
@@ -57,47 +110,31 @@ export async function removeAnnotations() {
|
|||||||
targetPageIndices = [...new Set(targetPageIndices)];
|
targetPageIndices = [...new Set(targetPageIndices)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetPageIndices.length === 0) throw new Error('No valid pages were selected.');
|
if (targetPageIndices.length === 0)
|
||||||
|
throw new Error('No valid pages were selected.');
|
||||||
|
|
||||||
const typesToRemove = new Set(
|
const typesToRemove = new Set(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(
|
||||||
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(cb => cb.value)
|
(cb) => (cb as HTMLInputElement).value
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typesToRemove.size === 0) throw new Error('Please select at least one annotation type to remove.');
|
if (typesToRemove.size === 0)
|
||||||
|
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(new Blob([newPdfBytes], { type: 'application/pdf' }), 'annotations-removed.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'annotations-removed.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Could not remove annotations. Please check your page range.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
e.message || 'Could not remove annotations. Please check your page range.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
import { PDFPageProxy } from 'pdfjs-dist/types/src/display/api.js';
|
||||||
|
|
||||||
|
let analysisCache = [];
|
||||||
|
|
||||||
|
async function isPageBlank(page: PDFPageProxy, threshold: number) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const viewport = page.getViewport({ scale: 0.2 });
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
|
||||||
|
await page.render({ canvasContext: context, viewport, canvas: canvas })
|
||||||
|
.promise;
|
||||||
|
|
||||||
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const data = imageData.data;
|
||||||
|
const totalPixels = data.length / 4;
|
||||||
|
let nonWhitePixels = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
if (data[i] < 245 || data[i + 1] < 245 || data[i + 2] < 245) {
|
||||||
|
nonWhitePixels++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blankness = 1 - nonWhitePixels / totalPixels;
|
||||||
|
return blankness >= threshold / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzePages() {
|
||||||
|
if (!state.pdfDoc) return;
|
||||||
|
showLoader('Analyzing for blank pages...');
|
||||||
|
|
||||||
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
|
const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||||
|
|
||||||
|
analysisCache = [];
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
promises.push(
|
||||||
|
pdf.getPage(i).then((page) =>
|
||||||
|
isPageBlank(page, 0).then((isActuallyBlank) => ({
|
||||||
|
pageNum: i,
|
||||||
|
isInitiallyBlank: isActuallyBlank,
|
||||||
|
pageRef: page,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
analysisCache = await Promise.all(promises);
|
||||||
|
hideLoader();
|
||||||
|
updateAnalysisUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAnalysisUI() {
|
||||||
|
const sensitivity = parseInt(
|
||||||
|
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
(
|
||||||
|
document.getElementById('sensitivity-value') as HTMLSpanElement
|
||||||
|
).textContent = sensitivity.toString();
|
||||||
|
|
||||||
|
const previewContainer = document.getElementById('analysis-preview');
|
||||||
|
const analysisText = document.getElementById('analysis-text');
|
||||||
|
const thumbnailsContainer = document.getElementById(
|
||||||
|
'removed-pages-thumbnails'
|
||||||
|
);
|
||||||
|
|
||||||
|
thumbnailsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
const pagesToRemove = [];
|
||||||
|
|
||||||
|
for (const pageData of analysisCache) {
|
||||||
|
const isConsideredBlank = await isPageBlank(pageData.pageRef, sensitivity);
|
||||||
|
if (isConsideredBlank) {
|
||||||
|
pagesToRemove.push(pageData.pageNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pagesToRemove.length > 0) {
|
||||||
|
analysisText.textContent = `Found ${pagesToRemove.length} blank page(s) to remove: ${pagesToRemove.join(', ')}`;
|
||||||
|
previewContainer.classList.remove('hidden');
|
||||||
|
|
||||||
|
for (const pageNum of pagesToRemove) {
|
||||||
|
const pageData = analysisCache[pageNum - 1];
|
||||||
|
const viewport = pageData.pageRef.getViewport({ scale: 0.1 });
|
||||||
|
const thumbCanvas = document.createElement('canvas');
|
||||||
|
thumbCanvas.width = viewport.width;
|
||||||
|
thumbCanvas.height = viewport.height;
|
||||||
|
await pageData.pageRef.render({
|
||||||
|
canvasContext: thumbCanvas.getContext('2d'),
|
||||||
|
viewport,
|
||||||
|
}).promise;
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = thumbCanvas.toDataURL();
|
||||||
|
img.className = 'rounded border border-gray-600';
|
||||||
|
img.title = `Page ${pageNum}`;
|
||||||
|
thumbnailsContainer.appendChild(img);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
analysisText.textContent =
|
||||||
|
'No blank pages found at this sensitivity level.';
|
||||||
|
previewContainer.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupRemoveBlankPagesTool() {
|
||||||
|
await analyzePages();
|
||||||
|
document
|
||||||
|
.getElementById('sensitivity-slider')
|
||||||
|
.addEventListener('input', updateAnalysisUI);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeBlankPages() {
|
||||||
|
showLoader('Removing blank pages...');
|
||||||
|
try {
|
||||||
|
const sensitivity = parseInt(
|
||||||
|
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
const indicesToKeep = [];
|
||||||
|
|
||||||
|
for (const pageData of analysisCache) {
|
||||||
|
const isConsideredBlank = await isPageBlank(
|
||||||
|
pageData.pageRef,
|
||||||
|
sensitivity
|
||||||
|
);
|
||||||
|
if (!isConsideredBlank) {
|
||||||
|
indicesToKeep.push(pageData.pageNum - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indicesToKeep.length === 0) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'No Content Found',
|
||||||
|
'All pages were identified as blank at the current sensitivity setting. No new file was created. Try lowering the sensitivity if you believe this is an error.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indicesToKeep.length === state.pdfDoc.getPageCount()) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'No Pages Removed',
|
||||||
|
'No pages were identified as blank at the current sensitivity level.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdf = await PDFDocument.create();
|
||||||
|
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||||
|
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||||
|
|
||||||
|
const newPdfBytes = await newPdf.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'non-blank.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not remove blank pages.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,60 @@
|
|||||||
|
|
||||||
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 async function removeMetadata() {
|
export function removeMetadataFromDoc(pdfDoc) {
|
||||||
showLoader('Removing all metadata...');
|
const infoDict = pdfDoc.getInfoDict();
|
||||||
try {
|
|
||||||
const infoDict = state.pdfDoc.getInfoDict();
|
|
||||||
|
|
||||||
const allKeys = infoDict.keys();
|
const allKeys = infoDict.keys();
|
||||||
allKeys.forEach((key: any) => {
|
allKeys.forEach((key: any) => {
|
||||||
infoDict.delete(key);
|
infoDict.delete(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
state.pdfDoc.setTitle('');
|
pdfDoc.setTitle('');
|
||||||
state.pdfDoc.setAuthor('');
|
pdfDoc.setAuthor('');
|
||||||
state.pdfDoc.setSubject('');
|
pdfDoc.setSubject('');
|
||||||
state.pdfDoc.setKeywords([]);
|
pdfDoc.setKeywords([]);
|
||||||
state.pdfDoc.setCreator('');
|
pdfDoc.setCreator('');
|
||||||
state.pdfDoc.setProducer('');
|
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() {
|
||||||
|
showLoader('Removing all metadata...');
|
||||||
|
try {
|
||||||
|
removeMetadataFromDoc(state.pdfDoc);
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-removed.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'metadata-removed.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,20 +3,40 @@ import { downloadFile } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
export async function reversePages() {
|
export async function reversePages() {
|
||||||
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
|
const pdfDocs = state.files.filter(
|
||||||
|
(file: File) => file.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
if (!pdfDocs.length) {
|
||||||
|
showAlert('Error', 'PDF not loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
showLoader('Reversing page order...');
|
showLoader('Reversing page order...');
|
||||||
try {
|
try {
|
||||||
|
const zip = new JSZip();
|
||||||
|
for (let j = 0; j < pdfDocs.length; j++) {
|
||||||
|
const file = pdfDocs[j];
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const pageCount = state.pdfDoc.getPageCount();
|
const pageCount = pdfDoc.getPageCount();
|
||||||
const reversedIndices = Array.from({ length: pageCount }, (_, i) => pageCount - 1 - i);
|
const reversedIndices = Array.from(
|
||||||
|
{ length: pageCount },
|
||||||
|
(_, i) => pageCount - 1 - i
|
||||||
|
);
|
||||||
|
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices);
|
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'reversed.pdf');
|
const originalName = file.name.replace(/\.pdf$/i, '');
|
||||||
|
const fileName = `${originalName}_reversed.pdf`;
|
||||||
|
zip.file(fileName, newPdfBytes);
|
||||||
|
}
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'reversed_pdfs.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not reverse the PDF pages.');
|
showAlert('Error', 'Could not reverse the PDF pages.');
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export async function rotate() {
|
|||||||
showLoader('Applying rotations...');
|
showLoader('Applying rotations...');
|
||||||
try {
|
try {
|
||||||
const pages = state.pdfDoc.getPages();
|
const pages = state.pdfDoc.getPages();
|
||||||
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
const pageIndex = parseInt(item.dataset.pageIndex);
|
const pageIndex = parseInt(item.dataset.pageIndex);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
@@ -20,7 +20,10 @@ export async function rotate() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const rotatedPdfBytes = await state.pdfDoc.save();
|
const rotatedPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([rotatedPdfBytes], { type: 'application/pdf' }), 'rotated.pdf');
|
downloadFile(
|
||||||
|
new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'rotated.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not apply rotations.');
|
showAlert('Error', 'Could not apply rotations.');
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+201
-69
@@ -4,19 +4,27 @@ import { state } from '../state.js';
|
|||||||
import html2canvas from 'html2canvas';
|
import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
const signState = {
|
const signState = {
|
||||||
pdf: null, canvas: null, context: null, pageRendering: false,
|
pdf: null,
|
||||||
currentPageNum: 1, scale: 1.0,
|
canvas: null,
|
||||||
|
context: null,
|
||||||
|
pageRendering: false,
|
||||||
|
currentPageNum: 1,
|
||||||
|
scale: 1.0,
|
||||||
pageSnapshot: null,
|
pageSnapshot: null,
|
||||||
drawCanvas: null, drawContext: null, isDrawing: false,
|
drawCanvas: null,
|
||||||
savedSignatures: [], placedSignatures: [], activeSignature: null,
|
drawContext: null,
|
||||||
|
isDrawing: false,
|
||||||
|
savedSignatures: [],
|
||||||
|
placedSignatures: [],
|
||||||
|
activeSignature: null,
|
||||||
interactionMode: 'none',
|
interactionMode: 'none',
|
||||||
draggedSigId: null,
|
draggedSigId: null,
|
||||||
dragOffsetX: 0, dragOffsetY: 0,
|
dragOffsetX: 0,
|
||||||
|
dragOffsetY: 0,
|
||||||
hoveredSigId: null,
|
hoveredSigId: null,
|
||||||
resizeHandle: null,
|
resizeHandle: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
async function renderPage(num: any) {
|
async function renderPage(num: any) {
|
||||||
signState.pageRendering = true;
|
signState.pageRendering = true;
|
||||||
const page = await signState.pdf.getPage(num);
|
const page = await signState.pdf.getPage(num);
|
||||||
@@ -25,7 +33,12 @@ async function renderPage(num: any) {
|
|||||||
signState.canvas.width = viewport.width;
|
signState.canvas.width = viewport.width;
|
||||||
await page.render({ canvasContext: signState.context, viewport }).promise;
|
await page.render({ canvasContext: signState.context, viewport }).promise;
|
||||||
|
|
||||||
signState.pageSnapshot = signState.context.getImageData(0, 0, signState.canvas.width, signState.canvas.height);
|
signState.pageSnapshot = signState.context.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
signState.canvas.width,
|
||||||
|
signState.canvas.height
|
||||||
|
);
|
||||||
|
|
||||||
drawSignatures();
|
drawSignatures();
|
||||||
|
|
||||||
@@ -38,11 +51,20 @@ function drawSignatures() {
|
|||||||
signState.context.putImageData(signState.pageSnapshot, 0, 0);
|
signState.context.putImageData(signState.pageSnapshot, 0, 0);
|
||||||
|
|
||||||
signState.placedSignatures
|
signState.placedSignatures
|
||||||
.filter(sig => sig.pageIndex === signState.currentPageNum - 1)
|
.filter((sig) => sig.pageIndex === signState.currentPageNum - 1)
|
||||||
.forEach(sig => {
|
.forEach((sig) => {
|
||||||
signState.context.drawImage(sig.image, sig.x, sig.y, sig.width, sig.height);
|
signState.context.drawImage(
|
||||||
|
sig.image,
|
||||||
|
sig.x,
|
||||||
|
sig.y,
|
||||||
|
sig.width,
|
||||||
|
sig.height
|
||||||
|
);
|
||||||
|
|
||||||
if (signState.hoveredSigId === sig.id || signState.draggedSigId === sig.id) {
|
if (
|
||||||
|
signState.hoveredSigId === sig.id ||
|
||||||
|
signState.draggedSigId === sig.id
|
||||||
|
) {
|
||||||
signState.context.strokeStyle = '#4f46e5';
|
signState.context.strokeStyle = '#4f46e5';
|
||||||
signState.context.setLineDash([6, 3]);
|
signState.context.setLineDash([6, 3]);
|
||||||
signState.context.strokeRect(sig.x, sig.y, sig.width, sig.height);
|
signState.context.strokeRect(sig.x, sig.y, sig.width, sig.height);
|
||||||
@@ -53,26 +75,29 @@ function drawSignatures() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function drawResizeHandles(sig: any) {
|
function drawResizeHandles(sig: any) {
|
||||||
const handleSize = 8;
|
const handleSize = 8;
|
||||||
const halfHandle = handleSize / 2;
|
const halfHandle = handleSize / 2;
|
||||||
const handles = getResizeHandles(sig);
|
const handles = getResizeHandles(sig);
|
||||||
signState.context.fillStyle = '#4f46e5';
|
signState.context.fillStyle = '#4f46e5';
|
||||||
Object.values(handles).forEach(handle => {
|
Object.values(handles).forEach((handle) => {
|
||||||
signState.context.fillRect(handle.x - halfHandle, handle.y - halfHandle, handleSize, handleSize);
|
signState.context.fillRect(
|
||||||
|
handle.x - halfHandle,
|
||||||
|
handle.y - halfHandle,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function fitToWidth() {
|
async function fitToWidth() {
|
||||||
const page = await signState.pdf.getPage(signState.currentPageNum);
|
const page = await signState.pdf.getPage(signState.currentPageNum);
|
||||||
const container = document.getElementById('canvas-container-sign');
|
const container = document.getElementById('canvas-container-sign');
|
||||||
signState.scale = container.clientWidth / page.getViewport({ scale: 1.0 }).width;
|
signState.scale =
|
||||||
|
container.clientWidth / page.getViewport({ scale: 1.0 }).width;
|
||||||
renderPage(signState.currentPageNum);
|
renderPage(signState.currentPageNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function setupDrawingCanvas() {
|
function setupDrawingCanvas() {
|
||||||
signState.drawCanvas = document.getElementById('signature-draw-canvas');
|
signState.drawCanvas = document.getElementById('signature-draw-canvas');
|
||||||
signState.drawContext = signState.drawCanvas.getContext('2d');
|
signState.drawContext = signState.drawCanvas.getContext('2d');
|
||||||
@@ -85,10 +110,12 @@ function setupDrawingCanvas() {
|
|||||||
signState.drawContext.lineWidth = 2;
|
signState.drawContext.lineWidth = 2;
|
||||||
|
|
||||||
const colorPicker = document.getElementById('signature-color');
|
const colorPicker = document.getElementById('signature-color');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
colorPicker.oninput = () => signState.drawContext.strokeStyle = colorPicker.value;
|
colorPicker.oninput = () =>
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
(signState.drawContext.strokeStyle = (
|
||||||
signState.drawContext.strokeStyle = colorPicker.value;
|
colorPicker as HTMLInputElement
|
||||||
|
).value);
|
||||||
|
signState.drawContext.strokeStyle = (colorPicker as HTMLInputElement).value;
|
||||||
|
|
||||||
const start = (e: any) => {
|
const start = (e: any) => {
|
||||||
signState.isDrawing = true;
|
signState.isDrawing = true;
|
||||||
@@ -103,18 +130,24 @@ function setupDrawingCanvas() {
|
|||||||
signState.drawContext.lineTo(pos.x, pos.y);
|
signState.drawContext.lineTo(pos.x, pos.y);
|
||||||
signState.drawContext.stroke();
|
signState.drawContext.stroke();
|
||||||
};
|
};
|
||||||
const stop = () => signState.isDrawing = false;
|
const stop = () => (signState.isDrawing = false);
|
||||||
|
|
||||||
['mousedown', 'touchstart'].forEach(evt => signState.drawCanvas.addEventListener(evt, start, { passive: false }));
|
['mousedown', 'touchstart'].forEach((evt) =>
|
||||||
['mousemove', 'touchmove'].forEach(evt => signState.drawCanvas.addEventListener(evt, draw, { passive: false }));
|
signState.drawCanvas.addEventListener(evt, start, { passive: false })
|
||||||
['mouseup', 'mouseleave', 'touchend'].forEach(evt => signState.drawCanvas.addEventListener(evt, stop));
|
);
|
||||||
|
['mousemove', 'touchmove'].forEach((evt) =>
|
||||||
|
signState.drawCanvas.addEventListener(evt, draw, { passive: false })
|
||||||
|
);
|
||||||
|
['mouseup', 'mouseleave', 'touchend'].forEach((evt) =>
|
||||||
|
signState.drawCanvas.addEventListener(evt, stop)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMousePos(canvas: any, evt: any) {
|
function getMousePos(canvas: any, evt: any) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
x: evt.clientX - rect.left,
|
x: evt.clientX - rect.left,
|
||||||
y: evt.clientY - rect.top
|
y: evt.clientY - rect.top,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,14 +163,17 @@ function renderSavedSignatures() {
|
|||||||
container.textContent = ''; //change
|
container.textContent = ''; //change
|
||||||
signState.savedSignatures.forEach((img, index) => {
|
signState.savedSignatures.forEach((img, index) => {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'saved-signature p-1 bg-white rounded-md cursor-pointer border-2 border-transparent hover:border-indigo-500 h-16';
|
wrapper.className =
|
||||||
|
'saved-signature p-1 bg-white rounded-md cursor-pointer border-2 border-transparent hover:border-indigo-500 h-16';
|
||||||
img.className = 'h-full w-auto mx-auto';
|
img.className = 'h-full w-auto mx-auto';
|
||||||
wrapper.appendChild(img);
|
wrapper.appendChild(img);
|
||||||
container.appendChild(wrapper);
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
wrapper.onclick = () => {
|
wrapper.onclick = () => {
|
||||||
signState.activeSignature = { image: img, index };
|
signState.activeSignature = { image: img, index };
|
||||||
document.querySelectorAll('.saved-signature').forEach(el => el.classList.remove('selected'));
|
document
|
||||||
|
.querySelectorAll('.saved-signature')
|
||||||
|
.forEach((el) => el.classList.remove('selected'));
|
||||||
wrapper.classList.add('selected');
|
wrapper.classList.add('selected');
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -156,12 +192,14 @@ function getResizeHandles(sig: any) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getHandleAtPos(pos: any, sig: any) {
|
function getHandleAtPos(pos: any, sig: any) {
|
||||||
const handles = getResizeHandles(sig);
|
const handles = getResizeHandles(sig);
|
||||||
const handleSize = 10;
|
const handleSize = 10;
|
||||||
for (const [name, handlePos] of Object.entries(handles)) {
|
for (const [name, handlePos] of Object.entries(handles)) {
|
||||||
if (Math.abs(pos.x - handlePos.x) < handleSize && Math.abs(pos.y - handlePos.y) < handleSize) {
|
if (
|
||||||
|
Math.abs(pos.x - handlePos.x) < handleSize &&
|
||||||
|
Math.abs(pos.y - handlePos.y) < handleSize
|
||||||
|
) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,23 +226,35 @@ function setupPlacementListeners() {
|
|||||||
let foundSigId: any = null;
|
let foundSigId: any = null;
|
||||||
let foundHandle = null;
|
let foundHandle = null;
|
||||||
|
|
||||||
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
|
signState.placedSignatures
|
||||||
|
.filter((s) => s.pageIndex === signState.currentPageNum - 1)
|
||||||
|
.reverse()
|
||||||
|
.forEach((sig) => {
|
||||||
if (foundSigId) return;
|
if (foundSigId) return;
|
||||||
const handle = getHandleAtPos(pos, sig);
|
const handle = getHandleAtPos(pos, sig);
|
||||||
if (handle) {
|
if (handle) {
|
||||||
foundSigId = sig.id;
|
foundSigId = sig.id;
|
||||||
foundHandle = handle;
|
foundHandle = handle;
|
||||||
} else if (pos.x >= sig.x && pos.x <= sig.x + sig.width && pos.y >= sig.y && pos.y <= sig.y + sig.height) {
|
} else if (
|
||||||
|
pos.x >= sig.x &&
|
||||||
|
pos.x <= sig.x + sig.width &&
|
||||||
|
pos.y >= sig.y &&
|
||||||
|
pos.y <= sig.y + sig.height
|
||||||
|
) {
|
||||||
foundSigId = sig.id;
|
foundSigId = sig.id;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.className = '';
|
canvas.className = '';
|
||||||
if (foundHandle) {
|
if (foundHandle) {
|
||||||
if (['top-left', 'bottom-right'].includes(foundHandle)) canvas.classList.add('resize-nwse');
|
if (['top-left', 'bottom-right'].includes(foundHandle))
|
||||||
else if (['top-right', 'bottom-left'].includes(foundHandle)) canvas.classList.add('resize-nesw');
|
canvas.classList.add('resize-nwse');
|
||||||
else if (['top-middle', 'bottom-middle'].includes(foundHandle)) canvas.classList.add('resize-ns');
|
else if (['top-right', 'bottom-left'].includes(foundHandle))
|
||||||
else if (['middle-left', 'middle-right'].includes(foundHandle)) canvas.classList.add('resize-ew');
|
canvas.classList.add('resize-nesw');
|
||||||
|
else if (['top-middle', 'bottom-middle'].includes(foundHandle))
|
||||||
|
canvas.classList.add('resize-ns');
|
||||||
|
else if (['middle-left', 'middle-right'].includes(foundHandle))
|
||||||
|
canvas.classList.add('resize-ew');
|
||||||
} else if (foundSigId) {
|
} else if (foundSigId) {
|
||||||
canvas.classList.add('movable');
|
canvas.classList.add('movable');
|
||||||
}
|
}
|
||||||
@@ -216,13 +266,18 @@ function setupPlacementListeners() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', mouseMoveHandler);
|
canvas.addEventListener('mousemove', mouseMoveHandler);
|
||||||
document.getElementById('canvas-container-sign').addEventListener('mouseleave', () => ghost.classList.add('hidden'));
|
document
|
||||||
|
.getElementById('canvas-container-sign')
|
||||||
|
.addEventListener('mouseleave', () => ghost.classList.add('hidden'));
|
||||||
|
|
||||||
const onDragStart = (e: any) => {
|
const onDragStart = (e: any) => {
|
||||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||||
let clickedOnSignature = false;
|
let clickedOnSignature = false;
|
||||||
|
|
||||||
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
|
signState.placedSignatures
|
||||||
|
.filter((s) => s.pageIndex === signState.currentPageNum - 1)
|
||||||
|
.reverse()
|
||||||
|
.forEach((sig) => {
|
||||||
if (clickedOnSignature) return;
|
if (clickedOnSignature) return;
|
||||||
const handle = getHandleAtPos(pos, sig);
|
const handle = getHandleAtPos(pos, sig);
|
||||||
if (handle) {
|
if (handle) {
|
||||||
@@ -230,7 +285,12 @@ function setupPlacementListeners() {
|
|||||||
signState.resizeHandle = handle;
|
signState.resizeHandle = handle;
|
||||||
signState.draggedSigId = sig.id;
|
signState.draggedSigId = sig.id;
|
||||||
clickedOnSignature = true;
|
clickedOnSignature = true;
|
||||||
} else if (pos.x >= sig.x && pos.x <= sig.x + sig.width && pos.y >= sig.y && pos.y <= sig.y + sig.height) {
|
} else if (
|
||||||
|
pos.x >= sig.x &&
|
||||||
|
pos.x <= sig.x + sig.width &&
|
||||||
|
pos.y >= sig.y &&
|
||||||
|
pos.y <= sig.y + sig.height
|
||||||
|
) {
|
||||||
signState.interactionMode = 'drag';
|
signState.interactionMode = 'drag';
|
||||||
signState.draggedSigId = sig.id;
|
signState.draggedSigId = sig.id;
|
||||||
signState.dragOffsetX = pos.x - sig.x;
|
signState.dragOffsetX = pos.x - sig.x;
|
||||||
@@ -243,11 +303,18 @@ function setupPlacementListeners() {
|
|||||||
ghost.classList.add('hidden');
|
ghost.classList.add('hidden');
|
||||||
} else if (signState.activeSignature) {
|
} else if (signState.activeSignature) {
|
||||||
const sigWidth = 150;
|
const sigWidth = 150;
|
||||||
const sigHeight = (signState.activeSignature.image.height / signState.activeSignature.image.width) * sigWidth;
|
const sigHeight =
|
||||||
|
(signState.activeSignature.image.height /
|
||||||
|
signState.activeSignature.image.width) *
|
||||||
|
sigWidth;
|
||||||
signState.placedSignatures.push({
|
signState.placedSignatures.push({
|
||||||
id: Date.now(), image: signState.activeSignature.image,
|
id: Date.now(),
|
||||||
x: pos.x - sigWidth / 2, y: pos.y - sigHeight / 2,
|
image: signState.activeSignature.image,
|
||||||
width: sigWidth, height: sigHeight, pageIndex: signState.currentPageNum - 1,
|
x: pos.x - sigWidth / 2,
|
||||||
|
y: pos.y - sigHeight / 2,
|
||||||
|
width: sigWidth,
|
||||||
|
height: sigHeight,
|
||||||
|
pageIndex: signState.currentPageNum - 1,
|
||||||
aspectRatio: sigWidth / sigHeight,
|
aspectRatio: sigWidth / sigHeight,
|
||||||
});
|
});
|
||||||
drawSignatures();
|
drawSignatures();
|
||||||
@@ -258,7 +325,9 @@ function setupPlacementListeners() {
|
|||||||
if (signState.interactionMode === 'none') return;
|
if (signState.interactionMode === 'none') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||||
const sig = signState.placedSignatures.find(s => s.id === signState.draggedSigId);
|
const sig = signState.placedSignatures.find(
|
||||||
|
(s) => s.id === signState.draggedSigId
|
||||||
|
);
|
||||||
if (!sig) return;
|
if (!sig) return;
|
||||||
|
|
||||||
if (signState.interactionMode === 'drag') {
|
if (signState.interactionMode === 'drag') {
|
||||||
@@ -268,8 +337,10 @@ function setupPlacementListeners() {
|
|||||||
const originalRight = sig.x + sig.width;
|
const originalRight = sig.x + sig.width;
|
||||||
const originalBottom = sig.y + sig.height;
|
const originalBottom = sig.y + sig.height;
|
||||||
|
|
||||||
if (signState.resizeHandle.includes('right')) sig.width = Math.max(20, pos.x - sig.x);
|
if (signState.resizeHandle.includes('right'))
|
||||||
if (signState.resizeHandle.includes('bottom')) sig.height = Math.max(20, pos.y - sig.y);
|
sig.width = Math.max(20, pos.x - sig.x);
|
||||||
|
if (signState.resizeHandle.includes('bottom'))
|
||||||
|
sig.height = Math.max(20, pos.y - sig.y);
|
||||||
if (signState.resizeHandle.includes('left')) {
|
if (signState.resizeHandle.includes('left')) {
|
||||||
sig.width = Math.max(20, originalRight - pos.x);
|
sig.width = Math.max(20, originalRight - pos.x);
|
||||||
sig.x = originalRight - sig.width;
|
sig.x = originalRight - sig.width;
|
||||||
@@ -279,9 +350,15 @@ function setupPlacementListeners() {
|
|||||||
sig.y = originalBottom - sig.height;
|
sig.y = originalBottom - sig.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (signState.resizeHandle.includes('left') || signState.resizeHandle.includes('right')) {
|
if (
|
||||||
|
signState.resizeHandle.includes('left') ||
|
||||||
|
signState.resizeHandle.includes('right')
|
||||||
|
) {
|
||||||
sig.height = sig.width / sig.aspectRatio;
|
sig.height = sig.width / sig.aspectRatio;
|
||||||
} else if (signState.resizeHandle.includes('top') || signState.resizeHandle.includes('bottom')) {
|
} else if (
|
||||||
|
signState.resizeHandle.includes('top') ||
|
||||||
|
signState.resizeHandle.includes('bottom')
|
||||||
|
) {
|
||||||
sig.width = sig.height * sig.aspectRatio;
|
sig.width = sig.height * sig.aspectRatio;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,9 +371,15 @@ function setupPlacementListeners() {
|
|||||||
drawSignatures();
|
drawSignatures();
|
||||||
};
|
};
|
||||||
|
|
||||||
['mousedown', 'touchstart'].forEach(evt => canvas.addEventListener(evt, onDragStart, { passive: false }));
|
['mousedown', 'touchstart'].forEach((evt) =>
|
||||||
['mousemove', 'touchmove'].forEach(evt => canvas.addEventListener(evt, onDragMove, { passive: false }));
|
canvas.addEventListener(evt, onDragStart, { passive: false })
|
||||||
['mouseup', 'mouseleave', 'touchend'].forEach(evt => canvas.addEventListener(evt, onDragEnd));
|
);
|
||||||
|
['mousemove', 'touchmove'].forEach((evt) =>
|
||||||
|
canvas.addEventListener(evt, onDragMove, { passive: false })
|
||||||
|
);
|
||||||
|
['mouseup', 'mouseleave', 'touchend'].forEach((evt) =>
|
||||||
|
canvas.addEventListener(evt, onDragEnd)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupSignTool() {
|
export async function setupSignTool() {
|
||||||
@@ -307,26 +390,46 @@ export async function setupSignTool() {
|
|||||||
const pdfData = await state.pdfDoc.save();
|
const pdfData = await state.pdfDoc.save();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
signState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
signState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
document.getElementById('total-pages-display-sign').textContent = signState.pdf.numPages;
|
document.getElementById('total-pages-display-sign').textContent =
|
||||||
|
signState.pdf.numPages;
|
||||||
|
|
||||||
await fitToWidth();
|
await fitToWidth();
|
||||||
setupDrawingCanvas();
|
setupDrawingCanvas();
|
||||||
setupPlacementListeners();
|
setupPlacementListeners();
|
||||||
|
|
||||||
document.getElementById('prev-page-sign').onclick = () => { if (signState.currentPageNum > 1) { signState.currentPageNum--; renderPage(signState.currentPageNum); } };
|
document.getElementById('prev-page-sign').onclick = () => {
|
||||||
document.getElementById('next-page-sign').onclick = () => { if (signState.currentPageNum < signState.pdf.numPages) { signState.currentPageNum++; renderPage(signState.currentPageNum); } };
|
if (signState.currentPageNum > 1) {
|
||||||
document.getElementById('zoom-in-btn').onclick = () => { signState.scale += 0.25; renderPage(signState.currentPageNum); };
|
signState.currentPageNum--;
|
||||||
document.getElementById('zoom-out-btn').onclick = () => { signState.scale = Math.max(0.25, signState.scale - 0.25); renderPage(signState.currentPageNum); };
|
renderPage(signState.currentPageNum);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.getElementById('next-page-sign').onclick = () => {
|
||||||
|
if (signState.currentPageNum < signState.pdf.numPages) {
|
||||||
|
signState.currentPageNum++;
|
||||||
|
renderPage(signState.currentPageNum);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.getElementById('zoom-in-btn').onclick = () => {
|
||||||
|
signState.scale += 0.25;
|
||||||
|
renderPage(signState.currentPageNum);
|
||||||
|
};
|
||||||
|
document.getElementById('zoom-out-btn').onclick = () => {
|
||||||
|
signState.scale = Math.max(0.25, signState.scale - 0.25);
|
||||||
|
renderPage(signState.currentPageNum);
|
||||||
|
};
|
||||||
document.getElementById('fit-width-btn').onclick = fitToWidth;
|
document.getElementById('fit-width-btn').onclick = fitToWidth;
|
||||||
document.getElementById('undo-btn').onclick = () => { signState.placedSignatures.pop(); drawSignatures(); };
|
document.getElementById('undo-btn').onclick = () => {
|
||||||
|
signState.placedSignatures.pop();
|
||||||
|
drawSignatures();
|
||||||
|
};
|
||||||
|
|
||||||
const tabs = ['draw', 'type', 'upload'];
|
const tabs = ['draw', 'type', 'upload'];
|
||||||
const tabButtons = tabs.map(t => document.getElementById(`${t}-tab-btn`));
|
const tabButtons = tabs.map((t) => document.getElementById(`${t}-tab-btn`));
|
||||||
const tabPanels = tabs.map(t => document.getElementById(`${t}-panel`));
|
const tabPanels = tabs.map((t) => document.getElementById(`${t}-panel`));
|
||||||
tabButtons.forEach((button, index) => {
|
tabButtons.forEach((button, index) => {
|
||||||
button.onclick = () => {
|
button.onclick = () => {
|
||||||
tabPanels.forEach(panel => panel.classList.add('hidden'));
|
tabPanels.forEach((panel) => panel.classList.add('hidden'));
|
||||||
tabButtons.forEach(btn => {
|
tabButtons.forEach((btn) => {
|
||||||
btn.classList.remove('border-indigo-500', 'text-white');
|
btn.classList.remove('border-indigo-500', 'text-white');
|
||||||
btn.classList.add('border-transparent', 'text-gray-400');
|
btn.classList.add('border-transparent', 'text-gray-400');
|
||||||
});
|
});
|
||||||
@@ -336,8 +439,22 @@ export async function setupSignTool() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('clear-draw-btn').onclick = () => signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height);
|
document.getElementById('clear-draw-btn').onclick = () =>
|
||||||
document.getElementById('save-draw-btn').onclick = () => { addSignatureToSaved(signState.drawCanvas.toDataURL()); signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height); };
|
signState.drawContext.clearRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
signState.drawCanvas.width,
|
||||||
|
signState.drawCanvas.height
|
||||||
|
);
|
||||||
|
document.getElementById('save-draw-btn').onclick = () => {
|
||||||
|
addSignatureToSaved(signState.drawCanvas.toDataURL());
|
||||||
|
signState.drawContext.clearRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
signState.drawCanvas.width,
|
||||||
|
signState.drawCanvas.height
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const textInput = document.getElementById('signature-text-input');
|
const textInput = document.getElementById('signature-text-input');
|
||||||
const fontPreview = document.getElementById('font-preview');
|
const fontPreview = document.getElementById('font-preview');
|
||||||
@@ -359,13 +476,18 @@ export async function setupSignTool() {
|
|||||||
fontSizeValue.textContent = fontSizeSlider.value;
|
fontSizeValue.textContent = fontSizeSlider.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
[textInput, fontFamilySelect, fontSizeSlider, fontColorPicker].forEach(el => el.addEventListener('input', updateFontPreview));
|
[textInput, fontFamilySelect, fontSizeSlider, fontColorPicker].forEach((el) =>
|
||||||
|
el.addEventListener('input', updateFontPreview)
|
||||||
|
);
|
||||||
updateFontPreview();
|
updateFontPreview();
|
||||||
|
|
||||||
document.getElementById('save-type-btn').onclick = async () => {
|
document.getElementById('save-type-btn').onclick = async () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
if (!textInput.value) return;
|
if (!textInput.value) return;
|
||||||
const canvas = await html2canvas(fontPreview, { backgroundColor: null, scale: 2 });
|
const canvas = await html2canvas(fontPreview, {
|
||||||
|
backgroundColor: null,
|
||||||
|
scale: 2,
|
||||||
|
});
|
||||||
addSignatureToSaved(canvas.toDataURL());
|
addSignatureToSaved(canvas.toDataURL());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -390,23 +512,33 @@ export async function applyAndSaveSignatures() {
|
|||||||
for (const sig of signState.placedSignatures) {
|
for (const sig of signState.placedSignatures) {
|
||||||
const page = pages[sig.pageIndex];
|
const page = pages[sig.pageIndex];
|
||||||
const originalPageSize = page.getSize();
|
const originalPageSize = page.getSize();
|
||||||
const pngBytes = await fetch(sig.image.src).then(res => res.arrayBuffer());
|
const pngBytes = await fetch(sig.image.src).then((res) =>
|
||||||
|
res.arrayBuffer()
|
||||||
|
);
|
||||||
const pngImage = await state.pdfDoc.embedPng(pngBytes);
|
const pngImage = await state.pdfDoc.embedPng(pngBytes);
|
||||||
|
|
||||||
const renderedPage = await signState.pdf.getPage(sig.pageIndex + 1);
|
const renderedPage = await signState.pdf.getPage(sig.pageIndex + 1);
|
||||||
const renderedViewport = renderedPage.getViewport({ scale: signState.scale });
|
const renderedViewport = renderedPage.getViewport({
|
||||||
|
scale: signState.scale,
|
||||||
|
});
|
||||||
const scaleRatio = originalPageSize.width / renderedViewport.width;
|
const scaleRatio = originalPageSize.width / renderedViewport.width;
|
||||||
|
|
||||||
page.drawImage(pngImage, {
|
page.drawImage(pngImage, {
|
||||||
x: sig.x * scaleRatio,
|
x: sig.x * scaleRatio,
|
||||||
y: originalPageSize.height - (sig.y * scaleRatio) - (sig.height * scaleRatio),
|
y:
|
||||||
|
originalPageSize.height -
|
||||||
|
sig.y * scaleRatio -
|
||||||
|
sig.height * scaleRatio,
|
||||||
width: sig.width * scaleRatio,
|
width: sig.width * scaleRatio,
|
||||||
height: sig.height * scaleRatio,
|
height: sig.height * scaleRatio,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'signed.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'signed.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to apply signatures.');
|
showAlert('Error', 'Failed to apply signatures.');
|
||||||
|
|||||||
@@ -42,8 +42,10 @@ export async function splitInHalf() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'split-half.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'split-half.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while splitting the PDF.');
|
showAlert('Error', 'An error occurred while splitting the PDF.');
|
||||||
|
|||||||
+43
-17
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
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';
|
||||||
@@ -9,7 +8,6 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
|||||||
// Track if visual selector has been rendered to avoid duplicates
|
// Track if visual selector has been rendered to avoid duplicates
|
||||||
let visualSelectorRendered = false;
|
let visualSelectorRendered = false;
|
||||||
|
|
||||||
|
|
||||||
async function renderVisualSelector() {
|
async function renderVisualSelector() {
|
||||||
if (visualSelectorRendered) return;
|
if (visualSelectorRendered) return;
|
||||||
|
|
||||||
@@ -32,10 +30,14 @@ async function renderVisualSelector() {
|
|||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport }).promise;
|
await page.render({
|
||||||
|
canvasContext: canvas.getContext('2d'),
|
||||||
|
viewport: viewport,
|
||||||
|
}).promise;
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
|
wrapper.className =
|
||||||
|
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
wrapper.dataset.pageIndex = i - 1;
|
wrapper.dataset.pageIndex = i - 1;
|
||||||
|
|
||||||
@@ -80,7 +82,6 @@ async function renderVisualSelector() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function setupSplitTool() {
|
export function setupSplitTool() {
|
||||||
const splitModeSelect = document.getElementById('split-mode');
|
const splitModeSelect = document.getElementById('split-mode');
|
||||||
const rangePanel = document.getElementById('range-panel');
|
const rangePanel = document.getElementById('range-panel');
|
||||||
@@ -122,12 +123,12 @@ export function setupSplitTool() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function split() {
|
export async function split() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const splitMode = document.getElementById('split-mode').value;
|
const splitMode = document.getElementById('split-mode').value;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
const downloadAsZip =
|
||||||
const downloadAsZip = document.getElementById('download-as-zip')?.checked || false;
|
(document.getElementById('download-as-zip') as HTMLInputElement)?.checked ||
|
||||||
|
false;
|
||||||
|
|
||||||
showLoader('Splitting PDF...');
|
showLoader('Splitting PDF...');
|
||||||
|
|
||||||
@@ -145,7 +146,14 @@ export async function split() {
|
|||||||
const trimmedRange = range.trim();
|
const trimmedRange = range.trim();
|
||||||
if (trimmedRange.includes('-')) {
|
if (trimmedRange.includes('-')) {
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
for (let i = start; i <= end; i++) indicesToExtract.push(i - 1);
|
for (let i = start; i <= end; i++) indicesToExtract.push(i - 1);
|
||||||
} else {
|
} else {
|
||||||
const pageNum = Number(trimmedRange);
|
const pageNum = Number(trimmedRange);
|
||||||
@@ -156,7 +164,9 @@ export async function split() {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'even-odd':
|
case 'even-odd':
|
||||||
const choiceElement = document.querySelector('input[name="even-odd-choice"]:checked');
|
const choiceElement = document.querySelector(
|
||||||
|
'input[name="even-odd-choice"]:checked'
|
||||||
|
);
|
||||||
if (!choiceElement) throw new Error('Please select even or odd pages.');
|
if (!choiceElement) throw new Error('Please select even or odd pages.');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||||
const choice = choiceElement.value;
|
const choice = choiceElement.value;
|
||||||
@@ -169,9 +179,11 @@ export async function split() {
|
|||||||
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
break;
|
break;
|
||||||
case 'visual':
|
case 'visual':
|
||||||
indicesToExtract = Array.from(document.querySelectorAll('.page-thumbnail-wrapper.selected'))
|
indicesToExtract = Array.from(
|
||||||
|
document.querySelectorAll('.page-thumbnail-wrapper.selected')
|
||||||
|
)
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
.map(el => parseInt(el.dataset.pageIndex));
|
.map((el) => parseInt(el.dataset.pageIndex));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,12 +192,17 @@ export async function split() {
|
|||||||
throw new Error('No pages were selected for splitting.');
|
throw new Error('No pages were selected for splitting.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (splitMode === 'all' || (['range', 'visual'].includes(splitMode) && downloadAsZip)) {
|
if (
|
||||||
|
splitMode === 'all' ||
|
||||||
|
(['range', 'visual'].includes(splitMode) && downloadAsZip)
|
||||||
|
) {
|
||||||
showLoader('Creating ZIP file...');
|
showLoader('Creating ZIP file...');
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
for (const index of uniqueIndices) {
|
for (const index of uniqueIndices) {
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
|
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
|
||||||
|
index as number,
|
||||||
|
]);
|
||||||
newPdf.addPage(copiedPage);
|
newPdf.addPage(copiedPage);
|
||||||
const pdfBytes = await newPdf.save();
|
const pdfBytes = await newPdf.save();
|
||||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||||
@@ -195,10 +212,16 @@ export async function split() {
|
|||||||
downloadFile(zipBlob, 'split-pages.zip');
|
downloadFile(zipBlob, 'split-pages.zip');
|
||||||
} else {
|
} else {
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, uniqueIndices as number[]);
|
const copiedPages = await newPdf.copyPages(
|
||||||
|
state.pdfDoc,
|
||||||
|
uniqueIndices as number[]
|
||||||
|
);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
const pdfBytes = await newPdf.save();
|
const pdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'split-document.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'split-document.pdf'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (splitMode === 'visual') {
|
if (splitMode === 'visual') {
|
||||||
@@ -206,7 +229,10 @@ export async function split() {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Failed to split PDF. Please check your selection.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
e.message || 'Failed to split PDF. Please check your selection.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ async function convertImageToPngBytes(file: any) {
|
|||||||
canvas.height = img.height;
|
canvas.height = img.height;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
|
const pngBlob = await new Promise((res) =>
|
||||||
|
canvas.toBlob(res, 'image/png')
|
||||||
|
);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
const pngBytes = await pngBlob.arrayBuffer();
|
||||||
resolve(pngBytes);
|
resolve(pngBytes);
|
||||||
@@ -42,13 +44,24 @@ export async function svgToPdf() {
|
|||||||
const pngBytes = await convertImageToPngBytes(file);
|
const pngBytes = await convertImageToPngBytes(file);
|
||||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
page.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: pngImage.width,
|
||||||
|
height: pngImage.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_svgs.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_svgs.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert SVG to PDF. One of the files may be invalid.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert SVG to PDF. One of the files may be invalid.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,19 +63,32 @@ export async function tiffToPdf() {
|
|||||||
|
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
const pngBlob = await new Promise<Blob>((res) => canvas.toBlob(res!, 'image/png'));
|
const pngBlob = await new Promise<Blob>((res) =>
|
||||||
|
canvas.toBlob(res!, 'image/png')
|
||||||
|
);
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
const pngBytes = await pngBlob.arrayBuffer();
|
||||||
|
|
||||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
page.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: pngImage.width,
|
||||||
|
height: pngImage.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_tiff.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_tiff.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||||
|
|
||||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts, PageSizes } from 'pdf-lib';
|
import {
|
||||||
|
PDFDocument as PDFLibDocument,
|
||||||
|
rgb,
|
||||||
|
StandardFonts,
|
||||||
|
PageSizes,
|
||||||
|
} from 'pdf-lib';
|
||||||
|
|
||||||
export async function txtToPdf() {
|
export async function txtToPdf() {
|
||||||
showLoader('Creating PDF...');
|
showLoader('Creating PDF...');
|
||||||
@@ -40,7 +45,8 @@ export async function txtToPdf() {
|
|||||||
const words = paragraph.split(' ');
|
const words = paragraph.split(' ');
|
||||||
let currentLine = '';
|
let currentLine = '';
|
||||||
for (const word of words) {
|
for (const word of words) {
|
||||||
const testLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
const testLine =
|
||||||
|
currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
||||||
if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) {
|
if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) {
|
||||||
currentLine = testLine;
|
currentLine = testLine;
|
||||||
} else {
|
} else {
|
||||||
@@ -48,7 +54,13 @@ export async function txtToPdf() {
|
|||||||
page = pdfDoc.addPage(pageSize);
|
page = pdfDoc.addPage(pageSize);
|
||||||
y = page.getHeight() - margin;
|
y = page.getHeight() - margin;
|
||||||
}
|
}
|
||||||
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
|
page.drawText(currentLine, {
|
||||||
|
x: margin,
|
||||||
|
y,
|
||||||
|
font,
|
||||||
|
size: fontSize,
|
||||||
|
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||||
|
});
|
||||||
y -= lineHeight;
|
y -= lineHeight;
|
||||||
currentLine = word;
|
currentLine = word;
|
||||||
}
|
}
|
||||||
@@ -58,13 +70,22 @@ export async function txtToPdf() {
|
|||||||
page = pdfDoc.addPage(pageSize);
|
page = pdfDoc.addPage(pageSize);
|
||||||
y = page.getHeight() - margin;
|
y = page.getHeight() - margin;
|
||||||
}
|
}
|
||||||
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
|
page.drawText(currentLine, {
|
||||||
|
x: margin,
|
||||||
|
y,
|
||||||
|
font,
|
||||||
|
size: fontSize,
|
||||||
|
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||||
|
});
|
||||||
y -= lineHeight;
|
y -= lineHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'text-document.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'text-document.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to create PDF from text.');
|
showAlert('Error', 'Failed to create PDF from text.');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// This tool doesn't have a "process" button. Its logic is handled directly in fileHandler.js after a file is uploaded.
|
// This tool doesn't have a "process" button. Its logic is handled directly in fileHandler.js after a file is uploaded.
|
||||||
export function viewMetadata() {
|
export function viewMetadata() {
|
||||||
console.log("");
|
console.log('');
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,9 @@ export async function webpToPdf() {
|
|||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.drawImage(imageBitmap, 0, 0);
|
ctx.drawImage(imageBitmap, 0, 0);
|
||||||
|
|
||||||
const pngBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
const pngBlob = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob(resolve, 'image/png')
|
||||||
|
);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
const pngBytes = await pngBlob.arrayBuffer();
|
||||||
|
|
||||||
@@ -38,10 +40,16 @@ export async function webpToPdf() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_webp.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_webp.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert WebP to PDF. Ensure all files are valid WebP images.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert WebP to PDF. Ensure all files are valid WebP images.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-15
@@ -13,20 +13,22 @@ export async function wordToPdf() {
|
|||||||
showLoader('Preparing preview...');
|
showLoader('Preparing preview...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const mammothOptions = {
|
const mammothOptions = {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||||
convertImage: mammoth.images.inline((element: any) => {
|
convertImage: mammoth.images.inline((element: any) => {
|
||||||
return element.read("base64").then((imageBuffer: any) => {
|
return element.read('base64').then((imageBuffer: any) => {
|
||||||
return {
|
return {
|
||||||
src: `data:${element.contentType};base64,${imageBuffer}`
|
src: `data:${element.contentType};base64,${imageBuffer}`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
};
|
};
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||||
const { value: html } = await mammoth.convertToHtml({ arrayBuffer }, mammothOptions);
|
const { value: html } = await mammoth.convertToHtml(
|
||||||
|
{ arrayBuffer },
|
||||||
|
mammothOptions
|
||||||
|
);
|
||||||
|
|
||||||
// Get references to our modal elements from index.html
|
// Get references to our modal elements from index.html
|
||||||
const previewModal = document.getElementById('preview-modal');
|
const previewModal = document.getElementById('preview-modal');
|
||||||
@@ -51,7 +53,7 @@ export async function wordToPdf() {
|
|||||||
previewContent.appendChild(marginDiv);
|
previewContent.appendChild(marginDiv);
|
||||||
|
|
||||||
const images = previewContent.querySelectorAll('img');
|
const images = previewContent.querySelectorAll('img');
|
||||||
const imagePromises = Array.from(images).map(img => {
|
const imagePromises = Array.from(images).map((img) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// @ts-expect-error TS(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
|
// @ts-expect-error TS(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
|
||||||
if (img.complete) resolve();
|
if (img.complete) resolve();
|
||||||
@@ -60,7 +62,6 @@ export async function wordToPdf() {
|
|||||||
});
|
});
|
||||||
await Promise.all(imagePromises);
|
await Promise.all(imagePromises);
|
||||||
|
|
||||||
|
|
||||||
previewModal.classList.remove('hidden');
|
previewModal.classList.remove('hidden');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ export async function wordToPdf() {
|
|||||||
const doc = new jsPDF({
|
const doc = new jsPDF({
|
||||||
orientation: 'p',
|
orientation: 'p',
|
||||||
unit: 'pt',
|
unit: 'pt',
|
||||||
format: 'letter'
|
format: 'letter',
|
||||||
});
|
});
|
||||||
|
|
||||||
await doc.html(previewContent, {
|
await doc.html(previewContent, {
|
||||||
@@ -81,7 +82,7 @@ export async function wordToPdf() {
|
|||||||
const pageHeight = doc.internal.pageSize.getHeight();
|
const pageHeight = doc.internal.pageSize.getHeight();
|
||||||
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
|
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
|
||||||
|
|
||||||
links.forEach(link => {
|
links.forEach((link) => {
|
||||||
if (!link.href) return;
|
if (!link.href) return;
|
||||||
|
|
||||||
const linkRect = link.getBoundingClientRect();
|
const linkRect = link.getBoundingClientRect();
|
||||||
@@ -95,13 +96,19 @@ export async function wordToPdf() {
|
|||||||
|
|
||||||
doc.setPage(pageNum);
|
doc.setPage(pageNum);
|
||||||
try {
|
try {
|
||||||
doc.link(relativeX + 45, yOnPage + 45, linkRect.width, linkRect.height, { url: link.href });
|
doc.link(
|
||||||
|
relativeX + 45,
|
||||||
|
yOnPage + 45,
|
||||||
|
linkRect.width,
|
||||||
|
linkRect.height,
|
||||||
|
{ url: link.href }
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Could not add link:", link.href, e);
|
console.warn('Could not add link:', link.href, e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const outputFileName = `${file.name.replace(/\.[^/.]+$/, "")}.pdf`;
|
const outputFileName = `${file.name.replace(/\.[^/.]+$/, '')}.pdf`;
|
||||||
doc.save(outputFileName);
|
doc.save(outputFileName);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
},
|
},
|
||||||
@@ -109,7 +116,7 @@ export async function wordToPdf() {
|
|||||||
x: 45,
|
x: 45,
|
||||||
y: 45,
|
y: 45,
|
||||||
width: 522,
|
width: 522,
|
||||||
windowWidth: previewContent.scrollWidth
|
windowWidth: previewContent.scrollWidth,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,10 +129,12 @@ export async function wordToPdf() {
|
|||||||
|
|
||||||
downloadBtn.addEventListener('click', downloadHandler);
|
downloadBtn.addEventListener('click', downloadHandler);
|
||||||
closeBtn.addEventListener('click', closeHandler);
|
closeBtn.addEventListener('click', closeHandler);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Preview Error', `Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`);
|
showAlert(
|
||||||
|
'Preview Error',
|
||||||
|
`Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+147
-9
@@ -3,7 +3,7 @@ import { dom, switchView, hideAlert } from './ui.js';
|
|||||||
import { setupToolInterface } from './handlers/toolSelectionHandler.js';
|
import { setupToolInterface } from './handlers/toolSelectionHandler.js';
|
||||||
import { createIcons, icons } from 'lucide';
|
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 init = () => {
|
const init = () => {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
@@ -11,9 +11,120 @@ const init = () => {
|
|||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
|
// Handle simple mode - hide branding sections but keep logo and copyright
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
dom.toolGrid.textContent = '';
|
dom.toolGrid.textContent = '';
|
||||||
|
|
||||||
categories.forEach(category => {
|
categories.forEach((category) => {
|
||||||
const categoryGroup = document.createElement('div');
|
const categoryGroup = document.createElement('div');
|
||||||
categoryGroup.className = 'category-group col-span-full';
|
categoryGroup.className = 'category-group col-span-full';
|
||||||
|
|
||||||
@@ -22,11 +133,13 @@ const init = () => {
|
|||||||
title.textContent = category.name;
|
title.textContent = category.name;
|
||||||
|
|
||||||
const toolsContainer = document.createElement('div');
|
const toolsContainer = document.createElement('div');
|
||||||
toolsContainer.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
|
toolsContainer.className =
|
||||||
|
'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
|
||||||
|
|
||||||
category.tools.forEach(tool => {
|
category.tools.forEach((tool) => {
|
||||||
const toolCard = document.createElement('div');
|
const toolCard = document.createElement('div');
|
||||||
toolCard.className = 'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center';
|
toolCard.className =
|
||||||
|
'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center';
|
||||||
toolCard.dataset.toolId = tool.id;
|
toolCard.dataset.toolId = tool.id;
|
||||||
|
|
||||||
const icon = document.createElement('i');
|
const icon = document.createElement('i');
|
||||||
@@ -60,14 +173,16 @@ const init = () => {
|
|||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const searchTerm = searchBar.value.toLowerCase().trim();
|
const searchTerm = searchBar.value.toLowerCase().trim();
|
||||||
|
|
||||||
categoryGroups.forEach(group => {
|
categoryGroups.forEach((group) => {
|
||||||
const toolCards = group.querySelectorAll('.tool-card');
|
const toolCards = group.querySelectorAll('.tool-card');
|
||||||
let visibleToolsInCategory = 0;
|
let visibleToolsInCategory = 0;
|
||||||
|
|
||||||
toolCards.forEach(card => {
|
toolCards.forEach((card) => {
|
||||||
const toolName = card.querySelector('h3').textContent.toLowerCase();
|
const toolName = card.querySelector('h3').textContent.toLowerCase();
|
||||||
const toolSubtitle = card.querySelector('p')?.textContent.toLowerCase() || '';
|
const toolSubtitle =
|
||||||
const isMatch = toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
|
card.querySelector('p')?.textContent.toLowerCase() || '';
|
||||||
|
const isMatch =
|
||||||
|
toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
|
||||||
|
|
||||||
card.classList.toggle('hidden', !isMatch);
|
card.classList.toggle('hidden', !isMatch);
|
||||||
if (isMatch) {
|
if (isMatch) {
|
||||||
@@ -79,6 +194,29 @@ const init = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener('keydown', function (e) {
|
||||||
|
const key = e.key.toLowerCase();
|
||||||
|
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
|
||||||
|
const isCtrlK = e.ctrlKey && key === 'k';
|
||||||
|
const isCmdK = isMac && e.metaKey && key === 'k';
|
||||||
|
|
||||||
|
if (isCtrlK || isCmdK) {
|
||||||
|
e.preventDefault();
|
||||||
|
searchBar.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const shortcutK = document.getElementById('shortcut');
|
||||||
|
const isIosOrAndroid = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
if (isIosOrAndroid) {
|
||||||
|
shortcutK.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
shortcutK.textContent = navigator.userAgent.toUpperCase().includes('MAC')
|
||||||
|
? '⌘ + K'
|
||||||
|
: 'Ctrl + K';
|
||||||
|
}
|
||||||
|
|
||||||
dom.toolGrid.addEventListener('click', (e) => {
|
dom.toolGrid.addEventListener('click', (e) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
|
||||||
const card = e.target.closest('.tool-card');
|
const card = e.target.closest('.tool-card');
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
|
const mobileMenu = document.getElementById('mobile-menu');
|
||||||
|
const menuIcon = document.getElementById('menu-icon');
|
||||||
|
const closeIcon = document.getElementById('close-icon');
|
||||||
|
|
||||||
|
if (mobileMenuButton && mobileMenu && menuIcon && closeIcon) {
|
||||||
|
// Toggle menu on button click
|
||||||
|
mobileMenuButton.addEventListener('click', () => {
|
||||||
|
const isExpanded =
|
||||||
|
mobileMenuButton.getAttribute('aria-expanded') === 'true';
|
||||||
|
|
||||||
|
// Toggle menu visibility
|
||||||
|
mobileMenu.classList.toggle('hidden');
|
||||||
|
|
||||||
|
// Toggle icons
|
||||||
|
menuIcon.classList.toggle('hidden');
|
||||||
|
closeIcon.classList.toggle('hidden');
|
||||||
|
|
||||||
|
// Update aria-expanded for accessibility
|
||||||
|
mobileMenuButton.setAttribute('aria-expanded', (!isExpanded).toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close menu when clicking on a link
|
||||||
|
const mobileLinks = mobileMenu.querySelectorAll('a');
|
||||||
|
mobileLinks.forEach((link) => {
|
||||||
|
link.addEventListener('click', () => {
|
||||||
|
mobileMenu.classList.add('hidden');
|
||||||
|
menuIcon.classList.remove('hidden');
|
||||||
|
closeIcon.classList.add('hidden');
|
||||||
|
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close menu when clicking outside
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const target = event.target as Node;
|
||||||
|
const isClickInsideMenu = mobileMenu.contains(target);
|
||||||
|
const isClickOnButton = mobileMenuButton.contains(target);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isClickInsideMenu &&
|
||||||
|
!isClickOnButton &&
|
||||||
|
!mobileMenu.classList.contains('hidden')
|
||||||
|
) {
|
||||||
|
mobileMenu.classList.add('hidden');
|
||||||
|
menuIcon.classList.remove('hidden');
|
||||||
|
closeIcon.classList.add('hidden');
|
||||||
|
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
+358
-61
@@ -1,7 +1,7 @@
|
|||||||
import { resetState } from './state.js';
|
import { resetState } from './state.js';
|
||||||
import { formatBytes } from './utils/helpers.js';
|
import { formatBytes } from './utils/helpers.js';
|
||||||
import { tesseractLanguages } from './config/tesseract-languages.js';
|
import { tesseractLanguages } from './config/tesseract-languages.js';
|
||||||
import { icons, createIcons } from "lucide";
|
import { icons, createIcons } from 'lucide';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
// Centralizing DOM element selection
|
// Centralizing DOM element selection
|
||||||
@@ -48,11 +48,11 @@ export const switchView = (view: any) => {
|
|||||||
dom.featuresSection.classList.remove('hidden');
|
dom.featuresSection.classList.remove('hidden');
|
||||||
dom.toolsHeader.classList.remove('hidden');
|
dom.toolsHeader.classList.remove('hidden');
|
||||||
// show dividers
|
// show dividers
|
||||||
dom.dividers.forEach(divider => {
|
dom.dividers.forEach((divider) => {
|
||||||
divider.classList.remove('hidden');
|
divider.classList.remove('hidden');
|
||||||
});
|
});
|
||||||
// show hideSections
|
// show hideSections
|
||||||
dom.hideSections.forEach(section => {
|
dom.hideSections.forEach((section) => {
|
||||||
section.classList.remove('hidden');
|
section.classList.remove('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,21 +63,19 @@ export const switchView = (view: any) => {
|
|||||||
dom.featuresSection.classList.add('hidden');
|
dom.featuresSection.classList.add('hidden');
|
||||||
dom.heroSection.classList.add('hidden');
|
dom.heroSection.classList.add('hidden');
|
||||||
dom.toolsHeader.classList.add('hidden');
|
dom.toolsHeader.classList.add('hidden');
|
||||||
dom.dividers.forEach(divider => {
|
dom.dividers.forEach((divider) => {
|
||||||
divider.classList.add('hidden');
|
divider.classList.add('hidden');
|
||||||
});
|
});
|
||||||
dom.hideSections.forEach(section => {
|
dom.hideSections.forEach((section) => {
|
||||||
section.classList.add('hidden');
|
section.classList.add('hidden');
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const thumbnailState = {
|
const thumbnailState = {
|
||||||
sortableInstances: {}
|
sortableInstances: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function initializeOrganizeSortable(containerId: any) {
|
function initializeOrganizeSortable(containerId: any) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -98,11 +96,10 @@ function initializeOrganizeSortable(containerId: any) {
|
|||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: any) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders page thumbnails for tools like 'Organize' and 'Rotate'.
|
* Renders page thumbnails for tools like 'Organize' and 'Rotate'.
|
||||||
* @param {string} toolId The ID of the active tool.
|
* @param {string} toolId The ID of the active tool.
|
||||||
@@ -135,7 +132,8 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
|||||||
wrapper.dataset.pageIndex = i - 1;
|
wrapper.dataset.pageIndex = i - 1;
|
||||||
|
|
||||||
const imgContainer = document.createElement('div');
|
const imgContainer = document.createElement('div');
|
||||||
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
imgContainer.className =
|
||||||
|
'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = canvas.toDataURL();
|
img.src = canvas.toDataURL();
|
||||||
@@ -148,11 +146,13 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
|||||||
wrapper.appendChild(imgContainer);
|
wrapper.appendChild(imgContainer);
|
||||||
|
|
||||||
const pageNumSpan = document.createElement('span');
|
const pageNumSpan = document.createElement('span');
|
||||||
pageNumSpan.className = 'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
pageNumSpan.className =
|
||||||
|
'absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||||
pageNumSpan.textContent = i.toString();
|
pageNumSpan.textContent = i.toString();
|
||||||
|
|
||||||
const deleteBtn = document.createElement('button');
|
const deleteBtn = document.createElement('button');
|
||||||
deleteBtn.className = 'delete-page-btn absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center';
|
deleteBtn.className =
|
||||||
|
'delete-page-btn absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center';
|
||||||
deleteBtn.innerHTML = '×';
|
deleteBtn.innerHTML = '×';
|
||||||
deleteBtn.addEventListener('click', (e) => {
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
(e.currentTarget as HTMLElement).parentElement.remove();
|
(e.currentTarget as HTMLElement).parentElement.remove();
|
||||||
@@ -174,12 +174,15 @@ export const renderPageThumbnails = async (toolId: any, pdfDoc: any) => {
|
|||||||
pageNumSpan.textContent = i.toString();
|
pageNumSpan.textContent = i.toString();
|
||||||
|
|
||||||
const rotateBtn = document.createElement('button');
|
const rotateBtn = document.createElement('button');
|
||||||
rotateBtn.className = 'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
|
rotateBtn.className =
|
||||||
|
'rotate-btn btn bg-gray-700 hover:bg-gray-600 p-2 rounded-full';
|
||||||
rotateBtn.title = 'Rotate 90°';
|
rotateBtn.title = 'Rotate 90°';
|
||||||
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>';
|
rotateBtn.innerHTML = '<i data-lucide="rotate-cw" class="w-5 h-5"></i>';
|
||||||
rotateBtn.addEventListener('click', (e) => {
|
rotateBtn.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const card = (e.currentTarget as HTMLElement).closest('.page-rotator-item') as HTMLElement;
|
const card = (e.currentTarget as HTMLElement).closest(
|
||||||
|
'.page-rotator-item'
|
||||||
|
) as HTMLElement;
|
||||||
const imgEl = card.querySelector('img');
|
const imgEl = card.querySelector('img');
|
||||||
let currentRotation = parseInt(card.dataset.rotation);
|
let currentRotation = parseInt(card.dataset.rotation);
|
||||||
currentRotation = (currentRotation + 90) % 360;
|
currentRotation = (currentRotation + 90) % 360;
|
||||||
@@ -212,7 +215,8 @@ export const renderFileDisplay = (container: any, files: any) => {
|
|||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
files.forEach((file: any) => {
|
files.forEach((file: any) => {
|
||||||
const fileDiv = document.createElement('div');
|
const fileDiv = document.createElement('div');
|
||||||
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
fileDiv.className =
|
||||||
|
'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm';
|
||||||
|
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate font-medium text-gray-200';
|
nameSpan.className = 'truncate font-medium text-gray-200';
|
||||||
@@ -247,7 +251,9 @@ const createFileInputHTML = (options = {}) => {
|
|||||||
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}">
|
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" ${multiple} accept="${acceptedFiles}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${showControls ? `
|
${
|
||||||
|
showControls
|
||||||
|
? `
|
||||||
<!-- NEW: Add control buttons for multi-file uploads -->
|
<!-- NEW: Add control buttons for multi-file uploads -->
|
||||||
<div id="file-controls" class="hidden mt-4 flex gap-3">
|
<div id="file-controls" class="hidden mt-4 flex gap-3">
|
||||||
<button id="add-more-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
<button id="add-more-btn" class="btn bg-indigo-600 hover:bg-indigo-700 text-white font-semibold px-4 py-2 rounded-lg flex items-center gap-2">
|
||||||
@@ -257,7 +263,9 @@ const createFileInputHTML = (options = {}) => {
|
|||||||
<i data-lucide="x"></i> Clear All
|
<i data-lucide="x"></i> Clear All
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -372,21 +380,71 @@ export const toolTemplates = {
|
|||||||
`,
|
`,
|
||||||
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">
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Required to open and view the PDF</p>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<label for="owner-password-input" class="block mb-2 text-sm font-medium text-gray-300">Owner Password (Optional)</label>
|
||||||
<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>
|
<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)">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
<button id="process-btn" class="btn-gradient w-full mt-6">Encrypt & Download</button>
|
<button id="process-btn" class="btn-gradient w-full mt-6">Encrypt & Download</button>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="pdf-canvas" class="hidden"></canvas>
|
|
||||||
`,
|
`,
|
||||||
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>
|
||||||
@@ -699,15 +757,17 @@ compress: () => `
|
|||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
'add-blank-page': () => `
|
'add-blank-page': () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Add Blank Page</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">Add Blank Pages</h2>
|
||||||
<p class="mb-6 text-gray-400">Insert a new blank page at a specific position in your document.</p>
|
<p class="mb-6 text-gray-400">Insert one or more blank pages at a specific position in your document.</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="blank-page-options" class="hidden mt-6">
|
<div id="blank-page-options" class="hidden mt-6">
|
||||||
<p class="mb-2 font-medium text-white">Total Pages: <span id="total-pages"></span></p>
|
<p class="mb-2 font-medium text-white">Total Pages: <span id="total-pages"></span></p>
|
||||||
<label for="page-number" class="block mb-2 text-sm font-medium text-gray-300">Insert blank page after page number:</label>
|
<label for="page-number" class="block mb-2 text-sm font-medium text-gray-300">Insert blank pages after page number:</label>
|
||||||
<input type="number" id="page-number" min="0" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 mb-6" placeholder="Enter 0 to add to the beginning">
|
<input type="number" id="page-number" min="0" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 mb-4" placeholder="Enter 0 to add to the beginning">
|
||||||
<button id="process-btn" class="btn-gradient w-full">Add Page & Download</button>
|
<label for="page-count" class="block mb-2 text-sm font-medium text-gray-300">Number of blank pages to insert:</label>
|
||||||
|
<input type="number" id="page-count" min="1" value="1" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 mb-6" placeholder="Enter number of pages">
|
||||||
|
<button id="process-btn" class="btn-gradient w-full">Add Pages & Download</button>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
'extract-pages': () => `
|
'extract-pages': () => `
|
||||||
@@ -787,7 +847,6 @@ compress: () => `
|
|||||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Add Watermark & Download</button>
|
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Add Watermark & Download</button>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
|
||||||
'add-header-footer': () => `
|
'add-header-footer': () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Add Header & Footer</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">Add Header & Footer</h2>
|
||||||
<p class="mb-6 text-gray-400">Add custom text to the top and bottom margins of every page.</p>
|
<p class="mb-6 text-gray-400">Add custom text to the top and bottom margins of every page.</p>
|
||||||
@@ -846,7 +905,6 @@ compress: () => `
|
|||||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Apply Header & Footer</button>
|
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Apply Header & Footer</button>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
|
||||||
'image-to-pdf': () => `
|
'image-to-pdf': () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">Image to PDF Converter</h2>
|
||||||
<p class="mb-6 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p>
|
<p class="mb-6 text-gray-400">Combine multiple images into a single PDF. Drag and drop to reorder.</p>
|
||||||
@@ -857,53 +915,73 @@ compress: () => `
|
|||||||
|
|
||||||
'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': () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">PDF to Markdown</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">PDF to Markdown</h2>
|
||||||
<p class="mb-6 text-gray-400">Convert a PDF's text content into a structured Markdown file.</p>
|
<p class="mb-6 text-gray-400">Convert a PDF's text content into a structured Markdown file.</p>
|
||||||
@@ -962,7 +1040,7 @@ compress: () => `
|
|||||||
'reverse-pages': () => `
|
'reverse-pages': () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">Reverse PDF Pages</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">Reverse PDF Pages</h2>
|
||||||
<p class="mb-6 text-gray-400">Flip the order of all pages in your document, making the last page the first.</p>
|
<p class="mb-6 text-gray-400">Flip the order of all pages in your document, making the last page the first.</p>
|
||||||
${createFileInputHTML()}
|
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
|
||||||
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Reverse & Download</button>
|
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Reverse & Download</button>
|
||||||
`,
|
`,
|
||||||
@@ -1360,6 +1438,17 @@ compress: () => `
|
|||||||
'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>
|
||||||
|
|
||||||
@@ -1369,12 +1458,16 @@ compress: () => `
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input type="text" id="lang-search" class="w-full bg-gray-900 border border-gray-600 text-white rounded-lg p-2.5 mb-2" placeholder="Search for languages...">
|
<input type="text" id="lang-search" class="w-full bg-gray-900 border border-gray-600 text-white rounded-lg p-2.5 mb-2" placeholder="Search for languages...">
|
||||||
<div id="lang-list" class="max-h-48 overflow-y-auto border border-gray-600 rounded-lg p-2 bg-gray-900">
|
<div id="lang-list" class="max-h-48 overflow-y-auto border border-gray-600 rounded-lg p-2 bg-gray-900">
|
||||||
${Object.entries(tesseractLanguages).map(([code, name]) => `
|
${Object.entries(tesseractLanguages)
|
||||||
|
.map(
|
||||||
|
([code, name]) => `
|
||||||
<label class="flex items-center gap-2 p-2 rounded-md hover:bg-gray-700 cursor-pointer">
|
<label class="flex items-center gap-2 p-2 rounded-md hover:bg-gray-700 cursor-pointer">
|
||||||
<input type="checkbox" value="${code}" class="lang-checkbox w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
|
<input type="checkbox" value="${code}" class="lang-checkbox w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500">
|
||||||
${name}
|
${name}
|
||||||
</label>
|
</label>
|
||||||
`).join('')}
|
`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs text-gray-500 mt-1">Selected: <span id="selected-langs-display" class="font-semibold">None</span></p>
|
<p class="text-xs text-gray-500 mt-1">Selected: <span id="selected-langs-display" class="font-semibold">None</span></p>
|
||||||
@@ -1382,7 +1475,10 @@ compress: () => `
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
@@ -1398,10 +1494,28 @@ compress: () => `
|
|||||||
<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>
|
||||||
@@ -1602,7 +1716,6 @@ compress: () => `
|
|||||||
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Remove Selected Annotations</button>
|
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Remove Selected Annotations</button>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
|
||||||
cropper: () => `
|
cropper: () => `
|
||||||
<h2 class="text-2xl font-bold text-white mb-4">PDF Cropper</h2>
|
<h2 class="text-2xl font-bold text-white mb-4">PDF Cropper</h2>
|
||||||
<p class="mb-6 text-gray-400">Upload a PDF to visually crop one or more pages. This tool offers a live preview and two distinct cropping modes.</p>
|
<p class="mb-6 text-gray-400">Upload a PDF to visually crop one or more pages. This tool offers a live preview and two distinct cropping modes.</p>
|
||||||
@@ -1794,4 +1907,188 @@ posterize: () => `
|
|||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
'remove-blank-pages': () => `
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4">Remove Blank Pages</h2>
|
||||||
|
<p class="mb-6 text-gray-400">Automatically detect and remove blank or nearly blank pages from your PDF. Adjust the sensitivity to control what is considered "blank".</p>
|
||||||
|
${createFileInputHTML()}
|
||||||
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
|
|
||||||
|
<div id="remove-blank-options" class="hidden mt-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="sensitivity-slider" class="block mb-2 text-sm font-medium text-gray-300">
|
||||||
|
Sensitivity (<span id="sensitivity-value">99</span>%)
|
||||||
|
</label>
|
||||||
|
<input type="range" id="sensitivity-slider" min="80" max="100" value="99" class="w-full">
|
||||||
|
<p class="text-xs text-gray-400 mt-1">Higher sensitivity requires pages to be more "blank" to be removed.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="analysis-preview" class="hidden p-4 bg-gray-900 border border-gray-700 rounded-lg">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-2">Analysis Results</h3>
|
||||||
|
<p id="analysis-text" class="text-gray-300"></p>
|
||||||
|
<div id="removed-pages-thumbnails" class="mt-4 grid grid-cols-5 sm:grid-cols-8 md:grid-cols-10 gap-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="process-btn" class="btn-gradient w-full mt-6">Remove Blank Pages & Download</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
'alternate-merge': () => `
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4">Alternate & Mix Pages</h2>
|
||||||
|
<p class="mb-6 text-gray-400">Combine pages from 2 or more documents, alternating between them. Drag the files to set the mixing order (e.g., Page 1 from Doc A, Page 1 from Doc B, Page 2 from Doc A, Page 2 from Doc B, etc.).</p>
|
||||||
|
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
|
||||||
|
|
||||||
|
<div id="alternate-merge-options" class="hidden mt-6">
|
||||||
|
<div class="p-3 bg-gray-900 rounded-lg border border-gray-700 mb-3">
|
||||||
|
<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>The tool will take one page from each document in the order you specify below, then repeat for the next page until all pages are used.</li>
|
||||||
|
<li>If a document runs out of pages, it will be skipped, and the tool will continue alternating with the remaining documents.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<ul id="alternate-file-list" class="space-y-2"></ul>
|
||||||
|
<button id="process-btn" class="btn-gradient w-full mt-6" disabled>Alternate & Mix PDFs</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
linearize: () => `
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4">Linearize PDFs (Fast Web View)</h2>
|
||||||
|
<p class="mb-6 text-gray-400">Optimize multiple PDFs for faster loading over the web. Files will be downloaded in a ZIP archive.</p>
|
||||||
|
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
|
||||||
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
|
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Linearize PDFs & Download ZIP</button>
|
||||||
|
`,
|
||||||
|
'add-attachments': () => `
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-4">Add Attachments to PDF</h2>
|
||||||
|
<p class="mb-6 text-gray-400">First, upload the PDF document you want to add files to.</p>
|
||||||
|
${createFileInputHTML({ accept: 'application/pdf' })}
|
||||||
|
<div id="file-display-area" class="mt-4 space-y-2"></div>
|
||||||
|
|
||||||
|
<div id="attachment-options" class="hidden mt-8">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-3">Upload Files to Attach</h3>
|
||||||
|
<p class="mb-4 text-gray-400">Select one or more files to embed within the PDF. You can attach any file type (images, documents, spreadsheets, etc.).</p>
|
||||||
|
|
||||||
|
<label for="attachment-files-input" class="w-full flex justify-center items-center px-6 py-10 bg-gray-900 text-gray-400 rounded-lg border-2 border-dashed border-gray-600 hover:bg-gray-800 hover:border-gray-500 cursor-pointer transition-colors">
|
||||||
|
<div class="text-center">
|
||||||
|
<svg class="mx-auto h-12 w-12" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true"><path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||||
|
<span class="mt-2 block text-sm font-medium">Click to upload files</span>
|
||||||
|
<span class="mt-1 block text-xs">Any file type, multiple files allowed</span>
|
||||||
|
</div>
|
||||||
|
<input id="attachment-files-input" name="attachment-files" type="file" class="sr-only" multiple>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div id="attachment-file-list" class="mt-4 space-y-2"></div>
|
||||||
|
|
||||||
|
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
'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>
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
+76
-11
@@ -1,17 +1,24 @@
|
|||||||
|
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 },
|
||||||
'Legal': { width: 612, height: 1008 },
|
Legal: { width: 612, height: 1008 },
|
||||||
'Tabloid': { width: 792, height: 1224 },
|
Tabloid: { width: 792, height: 1224 },
|
||||||
'A3': { width: 841.89, height: 1190.55 },
|
A3: { width: 841.89, height: 1190.55 },
|
||||||
'A5': { width: 419.53, height: 595.28 },
|
A5: { width: 419.53, height: 595.28 },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getStandardPageName(width: any, height: any) {
|
export function getStandardPageName(width: any, height: any) {
|
||||||
const tolerance = 1; // Allow for minor floating point variations
|
const tolerance = 1; // Allow for minor floating point variations
|
||||||
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
|
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
|
||||||
if ((Math.abs(width - size.width) < tolerance && Math.abs(height - size.height) < tolerance) ||
|
if (
|
||||||
(Math.abs(width - size.height) < tolerance && Math.abs(height - size.width) < tolerance)) {
|
(Math.abs(width - size.width) < tolerance &&
|
||||||
|
Math.abs(height - size.height) < tolerance) ||
|
||||||
|
(Math.abs(width - size.height) < tolerance &&
|
||||||
|
Math.abs(height - size.width) < tolerance)
|
||||||
|
) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,11 +46,13 @@ export function convertPoints(points: any, unit: any) {
|
|||||||
|
|
||||||
export const hexToRgb = (hex: any) => {
|
export const hexToRgb = (hex: any) => {
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
return result ? {
|
return result
|
||||||
|
? {
|
||||||
r: parseInt(result[1], 16) / 255,
|
r: parseInt(result[1], 16) / 255,
|
||||||
g: parseInt(result[2], 16) / 255,
|
g: parseInt(result[2], 16) / 255,
|
||||||
b: parseInt(result[3], 16) / 255,
|
b: parseInt(result[3], 16) / 255,
|
||||||
} : { r: 0, g: 0, b: 0 }; // Default to black
|
}
|
||||||
|
: { r: 0, g: 0, b: 0 }; // Default to black
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatBytes = (bytes: any, decimals = 1) => {
|
export const formatBytes = (bytes: any, decimals = 1) => {
|
||||||
@@ -89,7 +98,13 @@ export function parsePageRanges(rangeString: any, totalPages: any) {
|
|||||||
|
|
||||||
if (trimmedPart.includes('-')) {
|
if (trimmedPart.includes('-')) {
|
||||||
const [start, end] = trimmedPart.split('-').map(Number);
|
const [start, end] = trimmedPart.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) {
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
) {
|
||||||
console.warn(`Invalid range skipped: ${trimmedPart}`);
|
console.warn(`Invalid range skipped: ${trimmedPart}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -112,3 +127,53 @@ export function parsePageRanges(rangeString: any, totalPages: any) {
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { addBlankPage } from '@/js/logic/add-blank-page';
|
||||||
|
import * as ui from '@/js/ui';
|
||||||
|
import * as helpers from '@/js/utils/helpers';
|
||||||
|
import { state } from '@/js/state';
|
||||||
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
|
// -------------------- Mock Modules --------------------
|
||||||
|
vi.mock('@/js/ui', () => ({
|
||||||
|
showLoader: vi.fn(),
|
||||||
|
hideLoader: vi.fn(),
|
||||||
|
showAlert: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/js/utils/helpers', () => ({
|
||||||
|
downloadFile: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('pdf-lib', () => ({
|
||||||
|
PDFDocument: {
|
||||||
|
create: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// -------------------- Test Suite --------------------
|
||||||
|
describe('Add Blank Page Tool', () => {
|
||||||
|
let mockNewDoc: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset state pdfDoc
|
||||||
|
state.pdfDoc = {
|
||||||
|
getPageCount: () => 5,
|
||||||
|
getPage: vi.fn((index: number) => ({
|
||||||
|
getSize: () => ({ width: 595.28, height: 841.89 }), // A4 size
|
||||||
|
})),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Mock PDFDocument.create
|
||||||
|
mockNewDoc = {
|
||||||
|
copyPages: vi.fn((doc: any, indices: number[]) =>
|
||||||
|
Promise.resolve(indices.map((i: number) => ({ page: `page-${i}` })))
|
||||||
|
),
|
||||||
|
addPage: vi.fn(),
|
||||||
|
save: vi.fn(() => Promise.resolve(new Uint8Array([1, 2, 3]))),
|
||||||
|
};
|
||||||
|
vi.mocked(PDFLibDocument.create).mockResolvedValue(mockNewDoc);
|
||||||
|
|
||||||
|
// Mock helpers and UI
|
||||||
|
vi.mocked(helpers.downloadFile).mockImplementation(() => {});
|
||||||
|
vi.mocked(ui.showLoader).mockImplementation(() => {});
|
||||||
|
vi.mocked(ui.hideLoader).mockImplementation(() => {});
|
||||||
|
vi.mocked(ui.showAlert).mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- Input Validation Tests --------------------
|
||||||
|
describe('Input Validation', () => {
|
||||||
|
it('should show alert for empty page number', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Invalid Input',
|
||||||
|
'Please enter a page number.'
|
||||||
|
);
|
||||||
|
expect(ui.showLoader).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show alert for empty page count', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="2" />
|
||||||
|
<input id="page-count" value="" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Invalid Input',
|
||||||
|
'Please enter the number of pages to insert.'
|
||||||
|
);
|
||||||
|
expect(ui.showLoader).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show alert for invalid page number (negative)', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="-1" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Invalid Input',
|
||||||
|
'Please enter a number between 0 and 5.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show alert for invalid page number (too high)', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="10" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Invalid Input',
|
||||||
|
'Please enter a number between 0 and 5.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show alert for invalid page count (zero)', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="2" />
|
||||||
|
<input id="page-count" value="0" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Invalid Input',
|
||||||
|
'Please enter a valid number of pages (1 or more).'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show alert for invalid page count (negative)', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="2" />
|
||||||
|
<input id="page-count" value="-5" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Invalid Input',
|
||||||
|
'Please enter a valid number of pages (1 or more).'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- Single Page Insertion Tests --------------------
|
||||||
|
describe('Single Page Insertion', () => {
|
||||||
|
it('should add one blank page at the beginning', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="0" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showLoader).toHaveBeenCalledWith('Adding 1 blank page...');
|
||||||
|
expect(PDFLibDocument.create).toHaveBeenCalled();
|
||||||
|
// Should add 1 blank page + 5 existing pages = 6 total calls
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(6);
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalledWith([595.28, 841.89]);
|
||||||
|
expect(mockNewDoc.save).toHaveBeenCalled();
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(Blob),
|
||||||
|
'blank-page-added.pdf'
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add one blank page in the middle', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="2" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showLoader).toHaveBeenCalledWith('Adding 1 blank page...');
|
||||||
|
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [0, 1]);
|
||||||
|
// Should add 1 blank page + 5 existing pages = 6 total calls
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(6);
|
||||||
|
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(
|
||||||
|
state.pdfDoc,
|
||||||
|
[2, 3, 4]
|
||||||
|
);
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(Blob),
|
||||||
|
'blank-page-added.pdf'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add one blank page at the end', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="5" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(
|
||||||
|
state.pdfDoc,
|
||||||
|
[0, 1, 2, 3, 4]
|
||||||
|
);
|
||||||
|
// Should add 1 blank page + 5 existing pages = 6 total calls
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(6);
|
||||||
|
// When adding at the end, there are no pages after, so copyPages is not called for indicesAfter
|
||||||
|
expect(mockNewDoc.copyPages).toHaveBeenCalledTimes(1);
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(Blob),
|
||||||
|
'blank-page-added.pdf'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- Multiple Pages Insertion Tests --------------------
|
||||||
|
describe('Multiple Pages Insertion', () => {
|
||||||
|
it('should add multiple blank pages at the beginning', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="0" />
|
||||||
|
<input id="page-count" value="3" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showLoader).toHaveBeenCalledWith('Adding 3 blank pages...');
|
||||||
|
// Should add 3 blank pages + 5 existing pages = 8 total calls
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(8);
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(Blob),
|
||||||
|
'blank-pages-added.pdf'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add multiple blank pages in the middle', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="2" />
|
||||||
|
<input id="page-count" value="5" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showLoader).toHaveBeenCalledWith('Adding 5 blank pages...');
|
||||||
|
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(state.pdfDoc, [0, 1]);
|
||||||
|
// Should add 5 blank pages + 5 existing pages = 10 total calls
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(10);
|
||||||
|
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(
|
||||||
|
state.pdfDoc,
|
||||||
|
[2, 3, 4]
|
||||||
|
);
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(Blob),
|
||||||
|
'blank-pages-added.pdf'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add multiple blank pages at the end', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="5" />
|
||||||
|
<input id="page-count" value="2" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showLoader).toHaveBeenCalledWith('Adding 2 blank pages...');
|
||||||
|
expect(mockNewDoc.copyPages).toHaveBeenCalledWith(
|
||||||
|
state.pdfDoc,
|
||||||
|
[0, 1, 2, 3, 4]
|
||||||
|
);
|
||||||
|
// Should add 2 blank pages + 5 existing pages = 7 total calls
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(7);
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(Blob),
|
||||||
|
'blank-pages-added.pdf'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- Error Handling Tests --------------------
|
||||||
|
describe('Error Handling', () => {
|
||||||
|
it('should handle PDF creation errors', async () => {
|
||||||
|
vi.mocked(PDFLibDocument.create).mockRejectedValue(
|
||||||
|
new Error('PDF creation failed')
|
||||||
|
);
|
||||||
|
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="2" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
'Could not add blank page.'
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle PDF processing errors', async () => {
|
||||||
|
mockNewDoc.copyPages.mockRejectedValue(new Error('Copy failed'));
|
||||||
|
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="2" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
'Could not add blank page.'
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle save errors', async () => {
|
||||||
|
mockNewDoc.save.mockRejectedValue(new Error('Save failed'));
|
||||||
|
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="2" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
'Could not add blank page.'
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- Edge Cases Tests --------------------
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('should handle empty PDF (0 pages)', async () => {
|
||||||
|
state.pdfDoc.getPageCount = () => 0;
|
||||||
|
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="0" />
|
||||||
|
<input id="page-count" value="1" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
// When PDF has 0 pages, copyPages is not called at all
|
||||||
|
expect(mockNewDoc.copyPages).not.toHaveBeenCalled();
|
||||||
|
// Should add 1 blank page + 0 existing pages = 1 total call
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle large number of pages', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<input id="page-number" value="2" />
|
||||||
|
<input id="page-count" value="100" />
|
||||||
|
`;
|
||||||
|
|
||||||
|
await addBlankPage();
|
||||||
|
|
||||||
|
expect(ui.showLoader).toHaveBeenCalledWith('Adding 100 blank pages...');
|
||||||
|
// Should add 100 blank pages + 5 existing pages = 105 total calls
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalledTimes(105);
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(Blob),
|
||||||
|
'blank-pages-added.pdf'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { state } from '@/js/state';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
import Sortable from 'sortablejs';
|
||||||
|
import * as helpers from '@/js/utils/helpers';
|
||||||
|
import * as ui from '@/js/ui';
|
||||||
|
import {
|
||||||
|
setupAlternateMergeTool,
|
||||||
|
alternateMerge,
|
||||||
|
} from '@/js/logic/alternate-merge';
|
||||||
|
|
||||||
|
vi.mock('pdf-lib', () => ({
|
||||||
|
PDFDocument: {
|
||||||
|
create: vi.fn(),
|
||||||
|
load: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('sortablejs', () => ({
|
||||||
|
default: { create: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/js/utils/helpers', () => ({
|
||||||
|
readFileAsArrayBuffer: vi.fn(),
|
||||||
|
downloadFile: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/js/ui', () => ({
|
||||||
|
showLoader: vi.fn(),
|
||||||
|
hideLoader: vi.fn(),
|
||||||
|
showAlert: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Alternate Merge Tool', () => {
|
||||||
|
let mockPdfDoc1: any;
|
||||||
|
let mockPdfDoc2: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="alternate-merge-options" class="hidden"></div>
|
||||||
|
<button id="process-btn"></button>
|
||||||
|
<ul id="alternate-file-list"></ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
state.files = [
|
||||||
|
new File(['dummy1'], 'file1.pdf'),
|
||||||
|
new File(['dummy2'], 'file2.pdf'),
|
||||||
|
];
|
||||||
|
|
||||||
|
mockPdfDoc1 = { getPageCount: vi.fn(() => 2) };
|
||||||
|
mockPdfDoc2 = { getPageCount: vi.fn(() => 3) };
|
||||||
|
|
||||||
|
vi.mocked(helpers.readFileAsArrayBuffer).mockResolvedValue(
|
||||||
|
new ArrayBuffer(8)
|
||||||
|
);
|
||||||
|
vi.mocked(PDFDocument.load)
|
||||||
|
.mockResolvedValueOnce(mockPdfDoc1)
|
||||||
|
.mockResolvedValueOnce(mockPdfDoc2);
|
||||||
|
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setupAlternateMergeTool()', () => {
|
||||||
|
it('should initialize UI and load PDF files', async () => {
|
||||||
|
await setupAlternateMergeTool();
|
||||||
|
|
||||||
|
expect(ui.showLoader).toHaveBeenCalledWith('Loading PDF documents...');
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
expect(PDFDocument.load).toHaveBeenCalledTimes(2);
|
||||||
|
expect(document.querySelectorAll('#alternate-file-list li').length).toBe(
|
||||||
|
2
|
||||||
|
);
|
||||||
|
expect(Sortable.create).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show alert on load failure', async () => {
|
||||||
|
vi.mocked(PDFDocument.load).mockReset();
|
||||||
|
vi.mocked(PDFDocument.load).mockRejectedValueOnce(new Error('bad pdf'));
|
||||||
|
|
||||||
|
await setupAlternateMergeTool();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
expect.stringContaining('Failed to load one or more PDF files')
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('alternateMerge()', () => {
|
||||||
|
it('should show alert if less than 2 PDFs loaded', async () => {
|
||||||
|
// Setup with only 1 file - need to call setup first to populate internal state
|
||||||
|
state.files = [new File(['dummy1'], 'file1.pdf')];
|
||||||
|
vi.mocked(PDFDocument.load).mockReset();
|
||||||
|
vi.mocked(PDFDocument.load).mockResolvedValueOnce(mockPdfDoc1);
|
||||||
|
|
||||||
|
await setupAlternateMergeTool();
|
||||||
|
vi.clearAllMocks(); // Clear the setup calls
|
||||||
|
|
||||||
|
await alternateMerge();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Not Enough Files',
|
||||||
|
expect.stringContaining('Please upload at least two PDF files')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should merge pages alternately and download file', async () => {
|
||||||
|
// First setup the tool to populate internal state
|
||||||
|
await setupAlternateMergeTool();
|
||||||
|
vi.clearAllMocks(); // Clear setup calls
|
||||||
|
|
||||||
|
const mockCopyPages = vi.fn(() =>
|
||||||
|
Promise.resolve([{ page: 'mockPage' }] as any)
|
||||||
|
);
|
||||||
|
const mockAddPage = vi.fn();
|
||||||
|
const mockSave = vi.fn(() => Promise.resolve(new Uint8Array([1, 2, 3])));
|
||||||
|
|
||||||
|
vi.mocked(PDFDocument.create).mockResolvedValue({
|
||||||
|
copyPages: mockCopyPages,
|
||||||
|
addPage: mockAddPage,
|
||||||
|
save: mockSave,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const fileList = document.getElementById('alternate-file-list')!;
|
||||||
|
// The list should already be populated by setupAlternateMergeTool
|
||||||
|
// But ensure it has the correct structure
|
||||||
|
fileList.innerHTML = `
|
||||||
|
<li data-file-name="file1.pdf"></li>
|
||||||
|
<li data-file-name="file2.pdf"></li>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await alternateMerge();
|
||||||
|
|
||||||
|
expect(ui.showLoader).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Alternating')
|
||||||
|
);
|
||||||
|
expect(mockCopyPages).toHaveBeenCalled();
|
||||||
|
expect(mockAddPage).toHaveBeenCalled();
|
||||||
|
expect(mockSave).toHaveBeenCalled();
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalled();
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Success',
|
||||||
|
expect.stringContaining('mixed successfully')
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show alert on merge error', async () => {
|
||||||
|
// Setup the tool first to populate internal state with 2 PDFs
|
||||||
|
await setupAlternateMergeTool();
|
||||||
|
vi.clearAllMocks(); // Clear setup calls
|
||||||
|
|
||||||
|
// Mock PDFDocument.create to reject
|
||||||
|
vi.mocked(PDFDocument.create).mockRejectedValue(new Error('broken'));
|
||||||
|
|
||||||
|
await alternateMerge();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
expect.stringContaining('An error occurred while mixing')
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user