mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-06 16:18:39 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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,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
|
||||||
@@ -3,28 +3,44 @@ name: build docker image and push to dockerhub
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- 'main'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build-and-push:
|
docker-build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Set up QEMU
|
||||||
name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
-
|
- 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
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: bentopdf/bentopdf:latest
|
tags: |
|
||||||
|
bentopdf/bentopdf:latest
|
||||||
|
bentopdf/bentopdf:${{ steps.version.outputs.version_without_v }}
|
||||||
|
bentopdf/bentopdf:${{ steps.version.outputs.version }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
+51
-17
@@ -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.
|
||||||
@@ -11,17 +10,47 @@ This document outlines how to contribute, report issues, and get involved in the
|
|||||||
|
|
||||||
You can contribute in several ways:
|
You can contribute in several ways:
|
||||||
|
|
||||||
- **Reporting Bugs:** If you find a bug or unexpected behavior, please open an issue. Include steps to reproduce and any relevant screenshots or logs.
|
- **Reporting Bugs:** If you find a bug or unexpected behavior, please open an issue. Include steps to reproduce and any relevant screenshots or logs.
|
||||||
- **Feature Requests:** Suggest new features or improvements by opening an issue and describing your idea clearly.
|
- **Feature Requests:** Suggest new features or improvements by opening an issue and describing your idea clearly.
|
||||||
- **Code Contributions:** Submit a pull request with new features, bug fixes, or improvements.
|
- **Code Contributions:** Submit a pull request with new features, bug fixes, or improvements.
|
||||||
- **Documentation:** Help improve the README, usage examples, or guides.
|
- **Documentation:** Help improve the README, usage examples, or guides.
|
||||||
- **Testing:** Help test new releases or changes to ensure stability.
|
- **Testing:** Help test new releases or changes to ensure stability.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 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,48 +58,53 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Make Your Changes**
|
3. **Make Your Changes**
|
||||||
- Follow the code style and conventions used in the project.
|
- Follow the code style and conventions used in the project.
|
||||||
- Add comments where necessary.
|
- Add comments where necessary.
|
||||||
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Code Style
|
## 3. Code Style
|
||||||
|
|
||||||
- Follow the existing TypeScript and JavaScript conventions.
|
- Follow the existing TypeScript and JavaScript conventions.
|
||||||
- Use `camelCase` for variables and functions.
|
- Use `camelCase` for variables and functions.
|
||||||
- Keep lines reasonably short and readable.
|
- Keep lines reasonably short and readable.
|
||||||
- Comment complex logic for clarity.
|
- Comment complex logic for clarity.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Issues and Pull Requests
|
## 4. Issues and Pull Requests
|
||||||
|
|
||||||
- Make sure your PR is focused and addresses a single issue or feature.
|
- Make sure your PR is focused and addresses a single issue or feature.
|
||||||
- Reference related issues in your PR description (e.g., `Closes #12`).
|
- Reference related issues in your PR description (e.g., `Closes #12`).
|
||||||
- Be responsive to feedback and make requested changes promptly.
|
- Be responsive to feedback and make requested changes promptly.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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,18 +1,21 @@
|
|||||||
# 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) 
|
||||||
|
|
||||||
|
## ⭐ Stargazers over time
|
||||||
|
|
||||||
|
[](https://star-history.com/#alam00000/bentopdf&Date)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Why BentoPDF?
|
## ✨ Why BentoPDF?
|
||||||
|
|
||||||
- **Privacy First**: All processing happens in your browser. Your files are never uploaded to a server, guaranteeing 100% privacy.
|
- **Privacy First**: All processing happens in your browser. Your files are never uploaded to a server, guaranteeing 100% privacy.
|
||||||
- **No Limits**: Manipulate as many files as you want, as often you want. There are no restrictions or upload limits.
|
- **No Limits**: Manipulate as many files as you want, as often you want. There are no restrictions or upload limits.
|
||||||
- **High Performance**: Built with modern web technologies, BentoPDF is fast and efficient, handling even large PDF files with ease.
|
- **High Performance**: Built with modern web technologies, BentoPDF is fast and efficient, handling even large PDF files with ease.
|
||||||
- **Completely Free**: BentoPDF is a free and open-source tool for everyone.
|
- **Completely Free**: BentoPDF is a free and open-source tool for everyone.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -21,58 +24,63 @@
|
|||||||
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. |
|
| :----------------- | :------------------------------------------------------------------------- |
|
||||||
| **Split PDFs** | Extract specific pages or divide a document into smaller files. |
|
| **Merge PDFs** | Combine multiple PDF files into one. |
|
||||||
|
| **Split PDFs** | Extract specific pages or divide a document into smaller files. |
|
||||||
| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. |
|
| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. |
|
||||||
| **Extract Pages** | Save a specific range of pages as a new PDF. |
|
| **Extract Pages** | Save a specific range of pages as a new PDF. |
|
||||||
| **Delete Pages** | Remove unwanted pages from your document. |
|
| **Delete Pages** | Remove unwanted pages from your document. |
|
||||||
| **Rotate PDF** | Rotate individual or all pages in a document. |
|
| **Rotate PDF** | Rotate individual or all pages in a document. |
|
||||||
| **N-Up PDF** | Combine multiple pages onto a single page. |
|
| **N-Up PDF** | Combine multiple pages onto a single page. |
|
||||||
| **View PDF** | A powerful, integrated PDF viewer. |
|
| **View PDF** | A powerful, integrated PDF viewer. |
|
||||||
|
|
||||||
### Edit & Modify PDFs
|
### Edit & Modify PDFs
|
||||||
| Tool Name | Description |
|
|
||||||
| :--- | :--- |
|
| Tool Name | Description |
|
||||||
| **PDF Editor** | A comprehensive editor to modify your PDFs. |
|
| :--------------------- | :---------------------------------------------------------- |
|
||||||
| **Add Page Numbers** | Easily add page numbers with customizable formatting. |
|
| **PDF Editor** | A comprehensive editor to modify your PDFs. |
|
||||||
| **Add Watermark** | Add text or image watermarks to protect your documents. |
|
| **Add Page Numbers** | Easily add page numbers with customizable formatting. |
|
||||||
| **Header & Footer** | Add customizable headers and footers. |
|
| **Add Watermark** | Add text or image watermarks to protect your documents. |
|
||||||
| **Crop PDF** | Crop specific pages or the entire document. |
|
| **Header & Footer** | Add customizable headers and footers. |
|
||||||
| **Invert Colors** | Invert the colors of your PDF pages for better readability. |
|
| **Crop PDF** | Crop specific pages or the entire document. |
|
||||||
| **Change Background** | Modify the background color of your PDF. |
|
| **Invert Colors** | Invert the colors of your PDF pages for better readability. |
|
||||||
| **Change Text Color**| Change the color of text content within the PDF. |
|
| **Change Background** | Modify the background color of your PDF. |
|
||||||
| **Fill Forms** | Fill out PDF forms directly in your browser. |
|
| **Change Text Color** | Change the color of text content within the PDF. |
|
||||||
| **Flatten PDF** | Flatten form fields and annotations into static content. |
|
| **Fill Forms** | Fill out PDF forms directly in your browser. |
|
||||||
| **Remove Annotations**| Remove comments, highlights, and other annotations. |
|
| **Flatten PDF** | Flatten form fields and annotations into static content. |
|
||||||
|
| **Remove Annotations** | Remove comments, highlights, and other annotations. |
|
||||||
|
|
||||||
### 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. |
|
| :------------------ | :-------------------------------------------------------------- |
|
||||||
| **Markdown to PDF** | Convert `.md` files into professional PDF documents. |
|
| **Image to PDF** | Convert JPG, PNG, WebP, SVG, BMP, HEIC, and TIFF images to PDF. |
|
||||||
| **Text to PDF** | Convert plain text files into a PDF. |
|
| **Markdown to PDF** | Convert `.md` files into professional PDF documents. |
|
||||||
|
| **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 Greyscale** | Convert a color PDF into a black-and-white version. |
|
| **PDF to Image** | Convert PDF pages to JPG, PNG, WebP, BMP, or TIFF formats. |
|
||||||
| **OCR PDF** | Make scanned PDFs searchable and copyable using Optical Character Recognition. |
|
| **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. |
|
||||||
|
|
||||||
### Secure & Optimize PDFs
|
### Secure & Optimize PDFs
|
||||||
| Tool Name | Description |
|
|
||||||
| :--- | :--- |
|
| Tool Name | Description |
|
||||||
| **Compress PDF** | Reduce file size while maintaining quality. |
|
| :--------------------- | :----------------------------------------------------------------- |
|
||||||
| **Repair PDF** | Attempt to repair and recover data from a corrupted PDF. |
|
| **Compress PDF** | Reduce file size while maintaining quality. |
|
||||||
| **Encrypt PDF** | Add a password to protect your PDF from unauthorized access. |
|
| **Repair PDF** | Attempt to repair and recover data from a corrupted PDF. |
|
||||||
| **Decrypt PDF** | Remove password protection from a PDF (password required). |
|
| **Encrypt PDF** | Add a password to protect your PDF from unauthorized access. |
|
||||||
| **Change Permissions**| Set or modify user permissions for printing, copying, and editing. |
|
| **Decrypt PDF** | Remove password protection from a PDF (password required). |
|
||||||
| **Sign PDF** | Add your digital signature to a document. |
|
| **Change Permissions** | Set or modify user permissions for printing, copying, and editing. |
|
||||||
| **Redact Content** | Permanently remove sensitive content from your PDFs. |
|
| **Sign PDF** | Add your digital signature to a document. |
|
||||||
| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). |
|
| **Redact Content** | Permanently remove sensitive content from your PDFs. |
|
||||||
| **Remove Metadata** | Strip all metadata from your PDF for privacy. |
|
| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). |
|
||||||
|
| **Remove Metadata** | Strip all metadata from your PDF for privacy. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -81,72 +89,140 @@ 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)
|
|
||||||
- [npm](https://www.npmjs.com/) (or yarn/pnpm)
|
|
||||||
- [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/install/) (for containerized setup)
|
|
||||||
|
|
||||||
### 🚀 Run BentoPDF with Docker (Quick Start)
|
- [Node.js](https://nodejs.org/) (v18 or higher recommended)
|
||||||
You can run BentoPDF directly from Docker Hub without cloning the repository:
|
- [npm](https://www.npmjs.com/) (or yarn/pnpm)
|
||||||
|
- [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/install/) (for containerized setup)
|
||||||
|
|
||||||
|
### 🚀 Quick Start with Docker
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
1. **Clone the Repository**:
|
For a more robust setup with auto-restart capabilities:
|
||||||
```bash
|
|
||||||
git clone https://github.com/alam00000/bentopdf.git
|
|
||||||
cd bentopdf
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install Dependencies**:
|
1. **Download the repo and create a `docker-compose.yml` file or use the one given in repo**:
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Run the Development Server**:
|
```yaml
|
||||||
```bash
|
services:
|
||||||
npm run dev
|
bentopdf:
|
||||||
```
|
image: bentopdf/bentopdf:latest
|
||||||
The application will be available at `http://localhost:5173`.
|
container_name: bentopdf
|
||||||
|
ports:
|
||||||
|
- '3000:80'
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
### 🚀 Running with Docker Compose Locally
|
2. **Start the application**:
|
||||||
|
|
||||||
1. **Clone the Repository**:
|
```bash
|
||||||
```bash
|
docker-compose up -d
|
||||||
git clone https://github.com/alam00000/bentopdf.git
|
```
|
||||||
cd bentopdf
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Run with Docker Compose**:
|
The application will be available at `http://localhost:3000`.
|
||||||
Create a `docker-compose.yml` file with the content provided in the repository or use the existing one. Then, run:
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
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:
|
### 🏢 Simple Mode for Internal Use
|
||||||
```bash
|
|
||||||
docker-compose up --build -d
|
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.
|
||||||
```
|
|
||||||
This ensures your latest changes are applied inside the container
|
**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**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/alam00000/bentopdf.git
|
||||||
|
cd bentopdf
|
||||||
|
````
|
||||||
|
|
||||||
|
2. **Install Dependencies**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run the Development Server**:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
The application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
#### Option 2: Build and Run with Docker Compose
|
||||||
|
|
||||||
|
1. **Clone the Repository**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/alam00000/bentopdf.git
|
||||||
|
cd bentopdf
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run with Docker Compose**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:3000`.
|
||||||
|
|
||||||
|
> **Note:** After making any local changes to the code, rebuild the Docker image using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.dev.yml up --build -d
|
||||||
|
```
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
- **Vite**: A fast build tool for modern web development.
|
- **Vite**: A fast build tool for modern web development.
|
||||||
- **TypeScript**: For type safety and an improved developer experience.
|
- **TypeScript**: For type safety and an improved developer experience.
|
||||||
- **Tailwind CSS**: For rapid and consistent UI development.
|
- **Tailwind CSS**: For rapid and consistent UI development.
|
||||||
|
|
||||||
> **Note:** Some parts of the codebase still use legacy structures from the original implementation. Contributors should expect gradual updates as testing and refactoring continue.
|
> **Note:** Some parts of the codebase still use legacy structures from the original implementation. Contributors should expect gradual updates as testing and refactoring continue.
|
||||||
|
|
||||||
@@ -155,12 +231,15 @@ 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.
|
|
||||||
- **Markdown to PDF**: Enhanced support for converting `.md` files to PDF.
|
- **HTML to PDF**: Convert HTML files or web pages into PDF documents.
|
||||||
- **Sanitize PDF**: Remove potentially malicious content like scripts from PDFs.
|
- **Markdown to PDF**: Enhanced support for converting `.md` files to PDF.
|
||||||
- **Convert to PDF/A**: Convert PDFs to the PDF/A archival format.
|
- **Sanitize PDF**: Remove potentially malicious content like scripts from PDFs.
|
||||||
- **Edit PDF Content**: Directly edit text and other content within your PDF.
|
- **Convert to PDF/A**: Convert PDFs to the PDF/A archival format.
|
||||||
- **Linearize PDF**: Optimize PDFs for fast web viewing.
|
- **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/q42xWQmJ).
|
||||||
|
|
||||||
@@ -168,7 +247,7 @@ Contributions and discussions on the roadmap are welcome! Join the conversation
|
|||||||
|
|
||||||
## 🤝 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,7 +260,7 @@ 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.
|
||||||
|
|||||||
+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.
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Simple Mode for BentoPDF
|
||||||
|
|
||||||
|
Simple Mode is designed for internal organizational use where you want to hide all branding and marketing content, showing only the essential PDF tools for your users.
|
||||||
|
|
||||||
|
## What Simple Mode Does
|
||||||
|
|
||||||
|
When enabled, Simple Mode will:
|
||||||
|
|
||||||
|
- Hide the navigation bar
|
||||||
|
- Hide the hero section with marketing content
|
||||||
|
- Hide the features section
|
||||||
|
- Hide the security/compliance section
|
||||||
|
- Hide the FAQ section
|
||||||
|
- Hide the testimonials section
|
||||||
|
- Hide the support section
|
||||||
|
- Hide the footer
|
||||||
|
- Update the page title to "PDF Tools"
|
||||||
|
- Make the tools section more prominent
|
||||||
|
|
||||||
|
## How to Enable Simple Mode
|
||||||
|
|
||||||
|
### Using Docker Compose
|
||||||
|
|
||||||
|
1. Create a `.env` file in your project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SIMPLE_MODE=true
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run with docker-compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Docker Build
|
||||||
|
|
||||||
|
Build the image with the SIMPLE_MODE environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Environment Variables
|
||||||
|
|
||||||
|
Set the environment variable before building:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SIMPLE_MODE=true
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing Simple Mode Locally
|
||||||
|
|
||||||
|
After building with Simple Mode enabled, you need to serve the built files locally.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with simple mode
|
||||||
|
SIMPLE_MODE=true npm run build
|
||||||
|
|
||||||
|
# Serve the built files
|
||||||
|
npx serve dist -p 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
|
## 🔍 What to Look For
|
||||||
|
|
||||||
|
When Simple Mode is working correctly, you should see:
|
||||||
|
|
||||||
|
- ✅ Clean "PDF Tools" header (no marketing hero section)
|
||||||
|
- ✅ "Select a tool to get started" subtitle
|
||||||
|
- ✅ Search bar for tools
|
||||||
|
- ✅ All PDF tool cards organized by category
|
||||||
|
- ❌ No navigation bar
|
||||||
|
- ❌ No hero section with "The PDF Toolkit built for privacy"
|
||||||
|
- ❌ No features, FAQ, testimonials, or footer sections
|
||||||
|
|
||||||
|
## Example Docker Compose Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
image: bentopdf/bentopdf:latest
|
||||||
|
container_name: bentopdf
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '3000:80'
|
||||||
|
environment:
|
||||||
|
- SIMPLE_MODE=true
|
||||||
|
```
|
||||||
+324
-172
@@ -1,199 +1,351 @@
|
|||||||
<!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>
|
>
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<img
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
src="images/favicon.svg"
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
alt="Bento PDF Logo"
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
class="h-8 w-8"
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
/>
|
||||||
</div>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Desktop Navigation -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
</div>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Hamburger Button -->
|
||||||
|
<div class="md:hidden flex items-center">
|
||||||
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg md:text-xl text-gray-400">No compromises.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="about-hero" class="text-center py-16 md:py-24">
|
<div class="section-divider"></div>
|
||||||
<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>
|
|
||||||
</h1>
|
|
||||||
<p class="text-lg md:text-xl text-gray-400">
|
|
||||||
No compromises.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
|
||||||
|
<div class="text-center">
|
||||||
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
|
<i
|
||||||
<div class="text-center">
|
data-lucide="rocket"
|
||||||
<i data-lucide="rocket" class="w-16 h-16 text-indigo-400 mx-auto mb-6"></i>
|
class="w-16 h-16 text-indigo-400 mx-auto mb-6"
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Our Mission</h2>
|
></i>
|
||||||
<p class="text-lg text-gray-400 leading-relaxed">
|
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||||
To provide the most comprehensive PDF toolbox that respects your privacy and never asks for payment.
|
Our Mission
|
||||||
We believe essential document tools should be accessible to everyone, everywhere, without barriers.
|
</h2>
|
||||||
</p>
|
<p class="text-lg text-gray-400 leading-relaxed">
|
||||||
</div>
|
To provide the most comprehensive PDF toolbox that respects your
|
||||||
</section>
|
privacy and never asks for payment. We believe essential document
|
||||||
|
tools should be accessible to everyone, everywhere, without
|
||||||
<div class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700">
|
barriers.
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
</p>
|
||||||
<div class="text-center md:text-left">
|
|
||||||
<span class="text-indigo-400 font-bold uppercase">Our Core Philosophy</span>
|
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">Privacy First. Always.</h2>
|
|
||||||
<p class="text-gray-400 leading-relaxed">
|
|
||||||
In an era where data is a commodity, we take a different approach. All processing for Bentopdf
|
|
||||||
tools happens locally in your browser. This means your files never touch our servers, we never
|
|
||||||
see your documents, and we don't track what you do. Your documents remain completely and
|
|
||||||
unequivocally private. It's not just a feature; it's our foundation.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div class="relative w-48 h-48">
|
|
||||||
<div class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"></div>
|
|
||||||
<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>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="why-Bentopdf" class="py-16">
|
<div
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
|
class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700"
|
||||||
Why <span class="marker-slanted">Bentopdf?</span>
|
>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||||
|
<div class="text-center md:text-left">
|
||||||
|
<span class="text-indigo-400 font-bold uppercase"
|
||||||
|
>Our Core Philosophy</span
|
||||||
|
>
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">
|
||||||
|
Privacy First. Always.
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
|
<p class="text-gray-400 leading-relaxed">
|
||||||
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
In an era where data is a commodity, we take a different approach.
|
||||||
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
All processing for Bentopdf tools happens locally in your browser.
|
||||||
<div>
|
This means your files never touch our servers, we never see your
|
||||||
<h3 class="text-xl font-bold text-white">Built for Speed</h3>
|
documents, and we don't track what you do. Your documents remain
|
||||||
<p class="text-gray-400 mt-2">No waiting for uploads or downloads to a server. By processing
|
completely and unequivocally private. It's not just a feature;
|
||||||
files directly in your browser using modern web technologies like WebAssembly, we offer
|
it's our foundation.
|
||||||
unparalleled speed for all our tools.</p>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
<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
|
|
||||||
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>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
|
||||||
|
|
||||||
<section id="cta-section" class="text-center py-16">
|
|
||||||
<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">
|
|
||||||
Join thousands of users who trust Bentopdf for their daily document needs. Experience the difference
|
|
||||||
that privacy and performance can make.
|
|
||||||
</p>
|
</p>
|
||||||
<a href="index.html#tools-header"
|
</div>
|
||||||
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">
|
<div class="flex justify-center">
|
||||||
Explore All Tools
|
<div class="relative w-48 h-48">
|
||||||
</a>
|
<div
|
||||||
</section>
|
class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"
|
||||||
|
></div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<section id="why-Bentopdf" class="py-16">
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
|
||||||
|
Why <span class="marker-slanted">Bentopdf?</span>
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
|
||||||
|
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
||||||
|
<i
|
||||||
|
data-lucide="zap"
|
||||||
|
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
||||||
|
></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white">Built for Speed</h3>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
No waiting for uploads or downloads to a server. By processing
|
||||||
|
files directly in your browser using modern web technologies
|
||||||
|
like WebAssembly, we offer unparalleled speed for all our tools.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
<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
|
||||||
|
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>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
|
<section id="cta-section" class="text-center py-16">
|
||||||
|
<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">
|
||||||
|
Join thousands of users who trust Bentopdf for their daily document
|
||||||
|
needs. Experience the difference that privacy and performance can
|
||||||
|
make.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="index.html#tools-header"
|
||||||
|
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
Explore All Tools
|
||||||
|
</a>
|
||||||
|
</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
|
||||||
<div class="mb-8 md:mb-0">
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
<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">
|
<div class="mb-8 md:mb-0">
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
</div>
|
<img
|
||||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
src="images/favicon.svg"
|
||||||
</div>
|
alt="Bentopdf Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
<div>
|
/>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||||
<ul class="space-y-2 text-gray-400">
|
|
||||||
<li><a href="./about.html" class="hover:text-indigo-400">About Us</a></li>
|
|
||||||
<li><a href="./faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
|
||||||
<li><a href="./contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
|
||||||
<ul class="space-y-2 text-gray-400">
|
|
||||||
<li><a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
|
||||||
<li><a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
|
||||||
<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
|
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="./about.html" class="hover:text-indigo-400"
|
||||||
|
>About Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="./terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+195
-87
@@ -1,108 +1,216 @@
|
|||||||
<!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>
|
>
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<img
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
src="images/favicon.svg"
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
alt="Bento PDF Logo"
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
class="h-8 w-8"
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
/>
|
||||||
</div>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Desktop Navigation -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
</div>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Hamburger Button -->
|
||||||
|
<div class="md:hidden flex items-center">
|
||||||
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</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">
|
||||||
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
Get in Touch
|
||||||
We'd love to hear from you. Whether you have a question, feedback, or a feature request, please don't
|
</h1>
|
||||||
hesitate to reach out.
|
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
||||||
</p>
|
We'd love to hear from you. Whether you have a question, feedback, or
|
||||||
</section>
|
a feature request, please don't hesitate to reach out.
|
||||||
|
</p>
|
||||||
<div class="max-w-2xl mx-auto text-center py-8">
|
</section>
|
||||||
<p class="text-lg text-gray-400">
|
|
||||||
You can reach us directly by email at:
|
|
||||||
<a href="mailto:[email protected]"
|
|
||||||
class="text-indigo-400 underline hover:text-indigo-300">[email protected]</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="max-w-2xl mx-auto text-center py-8">
|
||||||
|
<p class="text-lg text-gray-400">
|
||||||
|
You can reach us directly by email at:
|
||||||
|
<a
|
||||||
|
href="mailto:[email protected]"
|
||||||
|
class="text-indigo-400 underline hover:text-indigo-300"
|
||||||
|
>[email protected]</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</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
|
||||||
<div class="mb-8 md:mb-0">
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
<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">
|
<div class="mb-8 md:mb-0">
|
||||||
<span class="text-xl font-bold text-white">Bentopdf</span>
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
</div>
|
<img
|
||||||
<p class="text-gray-400 text-sm">© 2025 Bentopdf. All rights reserved.</p>
|
src="images/favicon.svg"
|
||||||
</div>
|
alt="Bento PDF Logo"
|
||||||
<div>
|
class="h-10 w-10 mr-3"
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
/>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<span class="text-xl font-bold text-white">Bentopdf</span>
|
||||||
<li><a href="about.html" class="hover:text-indigo-400">About Us</a></li>
|
|
||||||
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
|
||||||
<li><a href="contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
|
||||||
<ul class="space-y-2 text-gray-400">
|
|
||||||
<li><a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
|
||||||
<li><a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
|
||||||
<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
|
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 Bentopdf. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="./terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
|
</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:
|
image: bentopdf/bentopdf:latest
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: bentopdf
|
container_name: bentopdf
|
||||||
ports:
|
|
||||||
- "3000:80"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '3000:80'
|
||||||
|
environment:
|
||||||
|
- SIMPLE_MODE=${SIMPLE_MODE:-false}
|
||||||
|
|||||||
@@ -1,211 +1,404 @@
|
|||||||
<!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>
|
>
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<img
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
src="images/favicon.svg"
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
alt="Bento PDF Logo"
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
class="h-8 w-8"
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
/>
|
||||||
</div>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Desktop Navigation -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
</div>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Hamburger Button -->
|
||||||
|
<div class="md:hidden flex items-center">
|
||||||
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</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">
|
||||||
|
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||||
|
Frequently Asked Questions
|
||||||
|
</h1>
|
||||||
|
<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.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="faq-hero" class="text-center py-16 md:py-24">
|
<div class="max-w-4xl mx-auto space-y-4">
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
<details
|
||||||
Frequently Asked Questions
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
</h1>
|
>
|
||||||
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
Have questions? We've got answers. Here are some of the most common things people ask about BentoPDF.
|
<h3 class="font-semibold text-white text-lg">
|
||||||
|
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>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
|
<p>
|
||||||
|
<strong>Absolutely.</strong> This is the most important feature of
|
||||||
|
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>
|
</p>
|
||||||
</section>
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto space-y-4">
|
<details
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
>
|
||||||
<h3 class="font-semibold text-white text-lg">Are my files safe and private?</h3>
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
</summary>
|
Is BentoPDF really free? What's the catch?
|
||||||
<div class="mt-4 text-gray-400">
|
</h3>
|
||||||
<p><strong>Absolutely.</strong> This is the most important feature of BentoPDF. All processing
|
<i
|
||||||
happens 100% locally in your web browser. Your files are never uploaded to any server, which
|
data-lucide="plus"
|
||||||
means we—or anyone else—can never see them. Your privacy is guaranteed.</p>
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
</div>
|
></i>
|
||||||
</details>
|
</summary>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<p>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
Yes, it's completely free, and there's no catch. There are no
|
||||||
<h3 class="font-semibold text-white text-lg">Is BentoPDF really free? What's the catch?</h3>
|
hidden fees, no subscription plans, no usage limits, and no
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
premium-only features. We believe essential tools should be
|
||||||
</summary>
|
accessible to everyone.
|
||||||
<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,
|
|
||||||
no usage limits, and no premium-only features. We believe essential tools should be accessible
|
|
||||||
to everyone.</p>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
|
||||||
<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>
|
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>After you load the website for the first time, most tools will work completely offline! Because
|
|
||||||
all the processing libraries are 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>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
|
||||||
<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>
|
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
|
||||||
</summary>
|
|
||||||
<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
|
|
||||||
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>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
|
||||||
<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>
|
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>Failures are rare but can happen for a few reasons:</p>
|
|
||||||
<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>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>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
|
||||||
<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>
|
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>We care about your privacy. BentoPDF does not track personal information. We use <a
|
|
||||||
href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank"
|
|
||||||
rel="noopener noreferrer">Simple Analytics</a> solely to see anonymous visit counts. 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>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
|
||||||
<h3 class="font-semibold text-white text-lg">What technology does BentoPDF use?</h3>
|
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
|
||||||
</summary>
|
|
||||||
<div class="mt-4 text-gray-400">
|
|
||||||
<p>BentoPDF is built on the power of modern web technologies. We primarily use JavaScript libraries
|
|
||||||
like <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>
|
|
||||||
</details>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-divider mt-16"></div>
|
|
||||||
|
|
||||||
<section id="contact-section" class="text-center py-16">
|
|
||||||
<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">
|
|
||||||
If you can't find the answer you're looking for, feel free to reach out to our support team. We're
|
|
||||||
always Bento to help.
|
|
||||||
</p>
|
</p>
|
||||||
<a href="contact.html"
|
</div>
|
||||||
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">
|
</details>
|
||||||
Contact Us
|
|
||||||
</a>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
|
<p>
|
||||||
|
After you load the website for the first time, most tools will
|
||||||
|
work completely offline! Because all the processing libraries are
|
||||||
|
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>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
|
</summary>
|
||||||
|
<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 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>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
|
<p>Failures are rare but can happen for a few reasons:</p>
|
||||||
|
<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>
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
|
<p>
|
||||||
|
We care about your privacy. BentoPDF does not track personal
|
||||||
|
information. We use
|
||||||
|
<a
|
||||||
|
href="https://simpleanalytics.com"
|
||||||
|
class="text-indigo-400 hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Simple Analytics</a
|
||||||
|
>
|
||||||
|
solely to see anonymous visit counts. 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>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
|
<h3 class="font-semibold text-white text-lg">
|
||||||
|
What technology does BentoPDF use?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
|
</summary>
|
||||||
|
<div class="mt-4 text-gray-400">
|
||||||
|
<p>
|
||||||
|
BentoPDF is built on the power of modern web technologies. We
|
||||||
|
primarily use JavaScript libraries like
|
||||||
|
<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>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-divider mt-16"></div>
|
||||||
|
|
||||||
|
<section id="contact-section" class="text-center py-16">
|
||||||
|
<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">
|
||||||
|
If you can't find the answer you're looking for, feel free to reach
|
||||||
|
out to our support team. We're always Bento to help.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
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
|
||||||
|
</a>
|
||||||
|
</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
|
||||||
<div class="mb-8 md:mb-0">
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
<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">
|
<div class="mb-8 md:mb-0">
|
||||||
<span class="text-xl font-bold text-white">Bento PDF</span>
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
</div>
|
<img
|
||||||
<p class="text-gray-400 text-sm">© 2025 Bento PDF. All rights reserved.</p>
|
src="images/favicon.svg"
|
||||||
</div>
|
alt="Bento PDF Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
<div>
|
/>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<span class="text-xl font-bold text-white">Bento PDF</span>
|
||||||
<ul class="space-y-2 text-gray-400">
|
|
||||||
<li><a href="about.html" class="hover:text-indigo-400">About Us</a></li>
|
|
||||||
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
|
||||||
<li><a href="contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
|
||||||
<ul class="space-y-2 text-gray-400">
|
|
||||||
<li><a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
|
||||||
<li><a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"><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
|
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 Bento PDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="./terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
><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
|
||||||
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+908
-555
File diff suppressed because it is too large
Load Diff
Generated
+24
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"blob-stream": "^0.1.3",
|
"blob-stream": "^0.1.3",
|
||||||
"cropperjs": "^1.6.1",
|
"cropperjs": "^1.6.1",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"@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.0",
|
||||||
|
"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.7",
|
||||||
@@ -2990,6 +2992,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",
|
||||||
@@ -9763,6 +9771,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",
|
||||||
|
|||||||
+8
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.0.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -14,7 +14,11 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
@@ -26,6 +30,7 @@
|
|||||||
"@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.0",
|
||||||
|
"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.7",
|
||||||
@@ -33,6 +38,7 @@
|
|||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"blob-stream": "^0.1.3",
|
"blob-stream": "^0.1.3",
|
||||||
"cropperjs": "^1.6.1",
|
"cropperjs": "^1.6.1",
|
||||||
|
|||||||
+298
-138
@@ -1,166 +1,326 @@
|
|||||||
<!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>
|
>
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<img
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
src="images/favicon.svg"
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
alt="Bento PDF Logo"
|
||||||
<a href="./contact.html" class="nav-link">Contact</a>
|
class="h-8 w-8"
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
/>
|
||||||
</div>
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Desktop Navigation -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
</div>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
</div>
|
<a href="./contact.html" class="nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Hamburger Button -->
|
||||||
|
<div class="md:hidden flex items-center">
|
||||||
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</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
|
||||||
<ul>
|
incapable of collecting the following information:
|
||||||
<li>The content of your PDF files or any other documents you use with our tools.</li>
|
</p>
|
||||||
<li>Any personal data contained within your documents.</li>
|
<ul>
|
||||||
<li>Filenames of your documents.</li>
|
<li>
|
||||||
<li>Any derived information or metadata from your files, beyond what is necessary for the tool to
|
The content of your PDF files or any other documents you use with
|
||||||
function during your active session (and this is immediately discarded).</li>
|
our tools.
|
||||||
</ul>
|
</li>
|
||||||
|
<li>Any personal data contained within 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 function during your active session
|
||||||
|
(and this is immediately discarded).
|
||||||
|
</li>
|
||||||
|
</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
|
||||||
<ul>
|
understand how users interact with our site, which tools are most
|
||||||
<li><strong>Usage Analytics:</strong> Anonymized data such as which tools are used, how often they
|
popular, and how we can improve the user experience. This includes:
|
||||||
are used, and which features are accessed. This is aggregated and cannot be tied back to an
|
</p>
|
||||||
individual user or document.</li>
|
<ul>
|
||||||
<li><strong>Performance Data:</strong> Anonymized error reports or performance metrics to help us
|
<li>
|
||||||
identify and fix bugs. This data contains no personal information or file content.</li>
|
<strong>Usage Analytics:</strong> Anonymized data such as which
|
||||||
</ul>
|
tools are used, how often they are used, and which features are
|
||||||
<p>We use privacy-respecting analytics platforms for this purpose. Specifically, we use
|
accessed. This is aggregated and cannot be tied back to an
|
||||||
<a href="https://simpleanalytics.com" target="_blank" rel="noopener noreferrer"
|
individual user or document.
|
||||||
class="text-indigo-400 hover:underline">Simple Analytics</a>
|
</li>
|
||||||
to track anonymous visit counts. This means we can see how many users visit our site, but
|
<li>
|
||||||
<strong>we never collect personal information or identify individual users</strong>.
|
<strong>Performance Data:</strong> Anonymized error reports or
|
||||||
Simple Analytics is fully GDPR-compliant and respects user privacy. We do not use tracking cookies
|
performance metrics to help us identify and fix bugs. This data
|
||||||
for advertising or cross-site profiling.
|
contains no personal information or file content.
|
||||||
</p>
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
We use privacy-respecting analytics platforms for this purpose.
|
||||||
|
Specifically, we use
|
||||||
|
<a
|
||||||
|
href="https://simpleanalytics.com"
|
||||||
|
target="_blank"
|
||||||
|
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>
|
||||||
|
|
||||||
<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:[email protected]">[email protected]</a>.</p>
|
If you have any questions about this Privacy Policy, please contact
|
||||||
</div>
|
us at
|
||||||
</section>
|
<a href="mailto:[email protected]">[email protected]</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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
|
||||||
<div class="mb-8 md:mb-0">
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
<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">
|
<div class="mb-8 md:mb-0">
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
</div>
|
<img
|
||||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
src="images/favicon.svg"
|
||||||
</div>
|
alt="BentoPDF Logo"
|
||||||
<div>
|
class="h-10 w-10 mr-3"
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
/>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||||
<li><a href="about.html" class="hover:text-indigo-400">About Us</a></li>
|
|
||||||
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
|
||||||
<li><a href="contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
|
||||||
<ul class="space-y-2 text-gray-400">
|
|
||||||
<li><a href="terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
|
||||||
<li><a href="privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
|
||||||
<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
|
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
|
<ul class="space-y-2 text-gray-400">
|
||||||
|
<li>
|
||||||
|
<a href="terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script>
|
<script>
|
||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
+32
-32
@@ -6,49 +6,49 @@ const sourceDir = path.join(__dirname, '../unoptimized-js');
|
|||||||
const outputDir = path.join(__dirname, '../dist/js');
|
const outputDir = path.join(__dirname, '../dist/js');
|
||||||
|
|
||||||
if (!fs.existsSync(outputDir)) {
|
if (!fs.existsSync(outputDir)) {
|
||||||
fs.mkdirSync(outputDir, { recursive: true });
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function processFile(filePath) {
|
function processFile(filePath) {
|
||||||
const relativePath = path.relative(sourceDir, filePath);
|
const relativePath = path.relative(sourceDir, filePath);
|
||||||
const outputPath = path.join(outputDir, relativePath);
|
const outputPath = path.join(outputDir, relativePath);
|
||||||
const outputDirForFile = path.dirname(outputPath);
|
const outputDirForFile = path.dirname(outputPath);
|
||||||
|
|
||||||
// Create subdirectories in the output folder
|
// Create subdirectories in the output folder
|
||||||
if (!fs.existsSync(outputDirForFile)) {
|
if (!fs.existsSync(outputDirForFile)) {
|
||||||
fs.mkdirSync(outputDirForFile, { recursive: true });
|
fs.mkdirSync(outputDirForFile, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Obfuscate the file
|
// 1. Obfuscate the file
|
||||||
console.log(`Obfuscating: ${filePath}`);
|
console.log(`Obfuscating: ${filePath}`);
|
||||||
const obfuscateCmd = `npx javascript-obfuscator "${filePath}" --output "${outputPath}"`;
|
const obfuscateCmd = `npx javascript-obfuscator "${filePath}" --output "${outputPath}"`;
|
||||||
execSync(obfuscateCmd);
|
execSync(obfuscateCmd);
|
||||||
|
|
||||||
// 2. Minify the obfuscated file using Terser
|
// 2. Minify the obfuscated file using Terser
|
||||||
console.log(`Minifying: ${outputPath}`);
|
console.log(`Minifying: ${outputPath}`);
|
||||||
const minifyCmd = `npx terser "${outputPath}" -o "${outputPath}" --compress --mangle`;
|
const minifyCmd = `npx terser "${outputPath}" -o "${outputPath}" --compress --mangle`;
|
||||||
execSync(minifyCmd);
|
execSync(minifyCmd);
|
||||||
|
|
||||||
console.log(`Success: ${outputPath}`);
|
console.log(`Success: ${outputPath}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error processing file ${filePath}:`, error.message);
|
console.error(`Error processing file ${filePath}:`, error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
processDirectory(filePath);
|
processDirectory(filePath);
|
||||||
} else if (path.extname(filePath) === '.js') {
|
} else if (path.extname(filePath) === '.js') {
|
||||||
if (filePath !== path.join(__dirname, 'build.js')) {
|
if (filePath !== path.join(__dirname, 'build.js')) {
|
||||||
processFile(filePath);
|
processFile(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Starting JavaScript build process...');
|
console.log('Starting JavaScript build process...');
|
||||||
|
|||||||
@@ -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();
|
||||||
+264
-254
@@ -1,458 +1,468 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'DM Sans', sans-serif;
|
font-family: 'DM Sans', sans-serif;
|
||||||
background-color: #111827;
|
background-color: #111827;
|
||||||
color: #d1d5db;
|
color: #d1d5db;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-card {
|
.tool-card {
|
||||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
transition:
|
||||||
border: 1px solid #374151;
|
transform 0.2s ease-in-out,
|
||||||
|
box-shadow 0.2s ease-in-out;
|
||||||
|
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:
|
||||||
border-color: #4f46e5;
|
0 10px 15px -3px rgba(0, 0, 0, 0.2),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||||
|
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 {
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
background: #1f2937;
|
background: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #4f46e5;
|
background: #4f46e5;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #4338ca;
|
background: #4338ca;
|
||||||
/* indigo-700 */
|
/* indigo-700 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style for drag-and-drop placeholder */
|
/* Style for drag-and-drop placeholder */
|
||||||
.sortable-ghost {
|
.sortable-ghost {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
border: 2px dashed #4f46e5;
|
border: 2px dashed #4f46e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#embed-pdf-container>div {
|
#embed-pdf-container > div {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tool-interface {
|
#tool-interface {
|
||||||
color: #39A0ED;
|
color: #39a0ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-thumbnail,
|
.page-thumbnail,
|
||||||
#file-list>li {
|
#file-list > li {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortable-chosen {
|
.sortable-chosen {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compare-viewer-wrapper.overlay-mode {
|
.compare-viewer-wrapper.overlay-mode {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 75vh;
|
height: 75vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 2px solid #374151;
|
border: 2px solid #374151;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: #1f2937;
|
background-color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This rule now ONLY applies to canvases in overlay mode */
|
/* This rule now ONLY applies to canvases in overlay mode */
|
||||||
.compare-viewer-wrapper.overlay-mode canvas {
|
.compare-viewer-wrapper.overlay-mode canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compare-viewer-wrapper.side-by-side-mode {
|
.compare-viewer-wrapper.side-by-side-mode {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-panel {
|
.pdf-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
height: 75vh;
|
height: 75vh;
|
||||||
border: 2px solid #374151;
|
border: 2px solid #374151;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: #1f2937;
|
background-color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This rule ensures canvases in side-by-side panels display at their natural rendered size. */
|
/* This rule ensures canvases in side-by-side panels display at their natural rendered size. */
|
||||||
.pdf-panel canvas {
|
.pdf-panel canvas {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer a {
|
footer a {
|
||||||
transition: color 0.2s ease-in-out;
|
transition: color 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker {
|
.marker {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker::after {
|
.marker::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* height: 30px; */
|
/* height: 30px; */
|
||||||
background-color: orange;
|
background-color: orange;
|
||||||
/* Yellow marker color */
|
/* Yellow marker color */
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
transform: skew(-20deg);
|
transform: skew(-20deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
background-color: #374151;
|
background-color: #374151;
|
||||||
/* bg-gray-700 */
|
/* bg-gray-700 */
|
||||||
color: #d1d5db;
|
color: #d1d5db;
|
||||||
/* text-gray-300 */
|
/* text-gray-300 */
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
/* rounded-full */
|
/* rounded-full */
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-button {
|
.cta-button {
|
||||||
background-color: #4f46e5;
|
background-color: #4f46e5;
|
||||||
/* indigo-600 */
|
/* indigo-600 */
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 12px 24px;
|
padding: 12px 24px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
/* rounded-lg */
|
/* rounded-lg */
|
||||||
transition: background-color 0.2s ease-in-out;
|
transition: background-color 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cta-button:hover {
|
.cta-button:hover {
|
||||||
background-color: #4338ca;
|
background-color: #4338ca;
|
||||||
/* indigo-700 */
|
/* indigo-700 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker-text {
|
.marker-text {
|
||||||
background-color: rgba(255, 255, 0, 0.5);
|
background-color: rgba(255, 255, 0, 0.5);
|
||||||
/* Yellow marker color */
|
/* Yellow marker color */
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-card {
|
.feature-card {
|
||||||
background-color: #1f2937;
|
background-color: #1f2937;
|
||||||
/* bg-gray-800 */
|
/* bg-gray-800 */
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
/* rounded-lg */
|
/* rounded-lg */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
@apply text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors;
|
@apply text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium transition-colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-nav-link {
|
.mobile-nav-link {
|
||||||
@apply text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium transition-colors;
|
@apply text-gray-300 hover:bg-gray-700 hover:text-white block px-3 py-2 rounded-md text-base font-medium transition-colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker-slanted {
|
.marker-slanted {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.marker-slanted::before {
|
.marker-slanted::before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
background: linear-gradient(120deg, #6366f1, #8b5cf6);
|
background: linear-gradient(120deg, #6366f1, #8b5cf6);
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-divider {
|
.section-divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(to right, transparent, #4f46e5, transparent);
|
background: linear-gradient(to right, transparent, #4f46e5, transparent);
|
||||||
/* Fades from transparent to indigo and back */
|
/* Fades from transparent to indigo and back */
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
/* my-16 */
|
/* my-16 */
|
||||||
max-width: 42rem;
|
max-width: 42rem;
|
||||||
/* max-w-xl */
|
/* max-w-xl */
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-gradient {
|
.btn-gradient {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.75rem 2rem;
|
padding: 0.75rem 2rem;
|
||||||
/* py-3 px-8 */
|
/* py-3 px-8 */
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
/* rounded-lg */
|
/* rounded-lg */
|
||||||
background-image: linear-gradient(to bottom, #6366f1, #4f46e5);
|
background-image: linear-gradient(to bottom, #6366f1, #4f46e5);
|
||||||
/* from-indigo-500 to-indigo-600 */
|
/* from-indigo-500 to-indigo-600 */
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
/* text-white */
|
/* text-white */
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
/* font-semibold */
|
/* font-semibold */
|
||||||
transition-property: all;
|
transition-property: all;
|
||||||
transition-duration: 200ms;
|
transition-duration: 200ms;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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:
|
||||||
/* hover:shadow-xl hover:shadow-indigo-500/30 */
|
0 10px 15px -3px rgba(79, 70, 229, 0.3),
|
||||||
transform: translateY(-0.25rem);
|
0 4px 6px -4px rgba(79, 70, 229, 0.3);
|
||||||
/* hover:-translate-y-1 */
|
/* hover:shadow-xl hover:shadow-indigo-500/30 */
|
||||||
|
transform: translateY(-0.25rem);
|
||||||
|
/* hover:-translate-y-1 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.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:
|
||||||
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
|
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 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-gradient:disabled {
|
.btn-gradient:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
transform: none;
|
transform: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solid-spinner {
|
.solid-spinner {
|
||||||
width: 64px;
|
width: 64px;
|
||||||
/* w-16 */
|
/* w-16 */
|
||||||
height: 64px;
|
height: 64px;
|
||||||
/* h-16 */
|
/* h-16 */
|
||||||
border: 5px solid #374151;
|
border: 5px solid #374151;
|
||||||
/* border-gray-700 */
|
/* border-gray-700 */
|
||||||
border-bottom-color: #4f46e5;
|
border-bottom-color: #4f46e5;
|
||||||
/* border-indigo-600 */
|
/* border-indigo-600 */
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#signature-ghost {
|
#signature-ghost {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
/* Allows clicks to pass through to the canvas */
|
/* Allows clicks to pass through to the canvas */
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Highlight for selected signature in the saved list */
|
/* Highlight for selected signature in the saved list */
|
||||||
.saved-signature.selected {
|
.saved-signature.selected {
|
||||||
border-color: #4f46e5;
|
border-color: #4f46e5;
|
||||||
/* indigo-600 */
|
/* indigo-600 */
|
||||||
box-shadow: 0 0 10px rgba(79, 70, 229, 0.5);
|
box-shadow: 0 0 10px rgba(79, 70, 229, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cursor change when hovering over a placed signature */
|
/* Cursor change when hovering over a placed signature */
|
||||||
#canvas-sign.movable {
|
#canvas-sign.movable {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas-sign.resize-ns {
|
#canvas-sign.resize-ns {
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas-sign.resize-ew {
|
#canvas-sign.resize-ew {
|
||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas-sign.resize-nesw {
|
#canvas-sign.resize-nesw {
|
||||||
cursor: nesw-resize;
|
cursor: nesw-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
#canvas-sign.resize-nwse {
|
#canvas-sign.resize-nwse {
|
||||||
cursor: nwse-resize;
|
cursor: nwse-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.faq-item.open .faq-question {
|
.faq-item.open .faq-question {
|
||||||
color: #818cf8;
|
color: #818cf8;
|
||||||
/* indigo-400 */
|
/* indigo-400 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.faq-item.open .faq-icon {
|
.faq-item.open .faq-icon {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Testimonial Card Styles */
|
/* Testimonial Card Styles */
|
||||||
.testimonial-card {
|
.testimonial-card {
|
||||||
background-color: #1f2937;
|
background-color: #1f2937;
|
||||||
/* bg-gray-800 */
|
/* bg-gray-800 */
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
/* rounded-xl */
|
/* rounded-xl */
|
||||||
border: 1px solid #374151;
|
border: 1px solid #374151;
|
||||||
/* border-gray-700 */
|
/* border-gray-700 */
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
background-color: #374151;
|
background-color: #374151;
|
||||||
/* bg-gray-700 */
|
/* bg-gray-700 */
|
||||||
color: #d1d5db;
|
color: #d1d5db;
|
||||||
/* text-gray-300 */
|
/* text-gray-300 */
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
/* rounded-full */
|
/* rounded-full */
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
background-color: #374151;
|
background-color: #374151;
|
||||||
/* bg-gray-700 */
|
/* bg-gray-700 */
|
||||||
color: #d1d5db;
|
color: #d1d5db;
|
||||||
/* text-gray-300 */
|
/* text-gray-300 */
|
||||||
/* Smaller padding and font size by default for mobile */
|
/* Smaller padding and font size by default for mobile */
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
/* rounded-full */
|
/* rounded-full */
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* On small screens (640px) and up, revert to the larger size */
|
/* On small screens (640px) and up, revert to the larger size */
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.pill {
|
.pill {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure form-field-group contents don't overflow on small screens */
|
/* Ensure form-field-group contents don't overflow on small screens */
|
||||||
.form-field-group .capitalize {
|
.form-field-group .capitalize {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
#form-fields-container {
|
#form-fields-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field-group {
|
.form-field-group {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
/* Adjust as needed */
|
/* Adjust as needed */
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#page-merge-preview {
|
#page-merge-preview {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-content h2 {
|
.legal-content h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: white;
|
color: white;
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-content h3 {
|
.legal-content h3 {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: rgb(129 140 248);
|
color: rgb(129 140 248);
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-content p {
|
.legal-content p {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
line-height: 1.625;
|
line-height: 1.625;
|
||||||
color: rgb(156 163 175);
|
color: rgb(156 163 175);
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-content ul {
|
.legal-content ul {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
list-style-position: inside;
|
list-style-position: inside;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
color: rgb(156 163 175);
|
color: rgb(156 163 175);
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-content a {
|
.legal-content a {
|
||||||
color: rgb(129 140 248);
|
color: rgb(129 140 248);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-content a:hover {
|
.legal-content a:hover {
|
||||||
color: rgb(165 180 252);
|
color: rgb(165 180 252);
|
||||||
}
|
}
|
||||||
|
|
||||||
details>summary {
|
details > summary {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
details>summary::-webkit-details-marker {
|
details > summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
details>summary .icon {
|
details > summary .icon {
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open]>summary .icon {
|
details[open] > summary .icon {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
+241
-211
@@ -1,23 +1,23 @@
|
|||||||
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,
|
||||||
canvas: null,
|
canvas: null,
|
||||||
context: null,
|
context: null,
|
||||||
container: null,
|
container: null,
|
||||||
currentPageNum: 1,
|
currentPageNum: 1,
|
||||||
pageRendering: false,
|
pageRendering: false,
|
||||||
pageNumPending: null,
|
pageNumPending: null,
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
pageSnapshot: null,
|
pageSnapshot: null,
|
||||||
isDrawing: false,
|
isDrawing: false,
|
||||||
startX: 0,
|
startX: 0,
|
||||||
startY: 0,
|
startY: 0,
|
||||||
cropBoxes: {},
|
cropBoxes: {},
|
||||||
lastInteractionRect: null, // Used to store the rectangle from the last move event
|
lastInteractionRect: null, // Used to store the rectangle from the last move event
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,9 +25,9 @@ const editorState = {
|
|||||||
* @param {PDFPageProxy} page - The PDF.js page object.
|
* @param {PDFPageProxy} page - The PDF.js page object.
|
||||||
*/
|
*/
|
||||||
function calculateFitScale(page: any) {
|
function calculateFitScale(page: any) {
|
||||||
const containerWidth = editorState.container.clientWidth;
|
const containerWidth = editorState.container.clientWidth;
|
||||||
const viewport = page.getViewport({ scale: 1.0 });
|
const viewport = page.getViewport({ scale: 1.0 });
|
||||||
return containerWidth / viewport.width;
|
return containerWidth / viewport.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,239 +35,269 @@ function calculateFitScale(page: any) {
|
|||||||
* @param {number} num The page number to render.
|
* @param {number} num The page number to render.
|
||||||
*/
|
*/
|
||||||
async function renderPage(num: any) {
|
async function renderPage(num: any) {
|
||||||
editorState.pageRendering = true;
|
editorState.pageRendering = true;
|
||||||
showLoader(`Loading page ${num}...`);
|
showLoader(`Loading page ${num}...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = await editorState.pdf.getPage(num);
|
const page = await editorState.pdf.getPage(num);
|
||||||
|
|
||||||
// @ts-expect-error TS(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
|
// @ts-expect-error TS(2367) FIXME: This condition will always return 'false' since th... Remove this comment to see the full error message
|
||||||
if (editorState.scale === 'fit') {
|
if (editorState.scale === 'fit') {
|
||||||
editorState.scale = calculateFitScale(page);
|
editorState.scale = calculateFitScale(page);
|
||||||
}
|
|
||||||
|
|
||||||
const viewport = page.getViewport({ scale: editorState.scale });
|
|
||||||
editorState.canvas.height = viewport.height;
|
|
||||||
editorState.canvas.width = viewport.width;
|
|
||||||
|
|
||||||
const renderContext = {
|
|
||||||
canvasContext: editorState.context,
|
|
||||||
viewport: viewport
|
|
||||||
};
|
|
||||||
|
|
||||||
await page.render(renderContext).promise;
|
|
||||||
|
|
||||||
editorState.pageSnapshot = editorState.context.getImageData(0, 0, editorState.canvas.width, editorState.canvas.height);
|
|
||||||
redrawShapes();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error rendering page:", error);
|
|
||||||
showAlert('Render Error', 'Could not display the page.');
|
|
||||||
} finally {
|
|
||||||
editorState.pageRendering = false;
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
document.getElementById('current-page-display').textContent = num;
|
|
||||||
// @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;
|
|
||||||
// @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;
|
|
||||||
|
|
||||||
if (editorState.pageNumPending !== null) {
|
|
||||||
const pendingPage = editorState.pageNumPending;
|
|
||||||
editorState.pageNumPending = null;
|
|
||||||
queueRenderPage(pendingPage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewport = page.getViewport({ scale: editorState.scale });
|
||||||
|
editorState.canvas.height = viewport.height;
|
||||||
|
editorState.canvas.width = viewport.width;
|
||||||
|
|
||||||
|
const renderContext = {
|
||||||
|
canvasContext: editorState.context,
|
||||||
|
viewport: viewport,
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.render(renderContext).promise;
|
||||||
|
|
||||||
|
editorState.pageSnapshot = editorState.context.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
editorState.canvas.width,
|
||||||
|
editorState.canvas.height
|
||||||
|
);
|
||||||
|
redrawShapes();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering page:', error);
|
||||||
|
showAlert('Render Error', 'Could not display the page.');
|
||||||
|
} finally {
|
||||||
|
editorState.pageRendering = false;
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
document.getElementById('current-page-display').textContent = num;
|
||||||
|
// @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;
|
||||||
|
// @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;
|
||||||
|
|
||||||
|
if (editorState.pageNumPending !== null) {
|
||||||
|
const pendingPage = editorState.pageNumPending;
|
||||||
|
editorState.pageNumPending = null;
|
||||||
|
queueRenderPage(pendingPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function queueRenderPage(num: any) {
|
function queueRenderPage(num: any) {
|
||||||
if (editorState.pageRendering) {
|
if (editorState.pageRendering) {
|
||||||
editorState.pageNumPending = num;
|
editorState.pageNumPending = num;
|
||||||
} else {
|
} else {
|
||||||
editorState.currentPageNum = num;
|
editorState.currentPageNum = num;
|
||||||
renderPage(num);
|
renderPage(num);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function redrawShapes() {
|
function redrawShapes() {
|
||||||
if (editorState.pageSnapshot) {
|
if (editorState.pageSnapshot) {
|
||||||
editorState.context.putImageData(editorState.pageSnapshot, 0, 0);
|
editorState.context.putImageData(editorState.pageSnapshot, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1];
|
|
||||||
if (currentCropBox) {
|
|
||||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
|
||||||
editorState.context.lineWidth = 2;
|
|
||||||
editorState.context.setLineDash([8, 4]);
|
|
||||||
editorState.context.strokeRect(currentCropBox.x, currentCropBox.y, currentCropBox.width, currentCropBox.height);
|
|
||||||
editorState.context.setLineDash([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEventCoordinates(e: any) {
|
|
||||||
const rect = editorState.canvas.getBoundingClientRect();
|
|
||||||
const touch = e.touches ? e.touches[0] : e;
|
|
||||||
const scaleX = editorState.canvas.width / rect.width;
|
|
||||||
const scaleY = editorState.canvas.height / rect.height;
|
|
||||||
return {
|
|
||||||
x: (touch.clientX - rect.left) * scaleX,
|
|
||||||
y: (touch.clientY - rect.top) * scaleY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInteractionStart(e: any) {
|
|
||||||
e.preventDefault();
|
|
||||||
const coords = getEventCoordinates(e);
|
|
||||||
editorState.isDrawing = true;
|
|
||||||
editorState.startX = coords.x;
|
|
||||||
editorState.startY = coords.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInteractionMove(e: any) {
|
|
||||||
if (!editorState.isDrawing) return;
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
redrawShapes();
|
|
||||||
const coords = getEventCoordinates(e);
|
|
||||||
|
|
||||||
const x = Math.min(editorState.startX, coords.x);
|
|
||||||
const y = Math.min(editorState.startY, coords.y);
|
|
||||||
const width = Math.abs(editorState.startX - coords.x);
|
|
||||||
const height = Math.abs(editorState.startY - coords.y);
|
|
||||||
|
|
||||||
|
const currentCropBox = editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||||
|
if (currentCropBox) {
|
||||||
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(x, y, width, height);
|
editorState.context.strokeRect(
|
||||||
|
currentCropBox.x,
|
||||||
|
currentCropBox.y,
|
||||||
|
currentCropBox.width,
|
||||||
|
currentCropBox.height
|
||||||
|
);
|
||||||
editorState.context.setLineDash([]);
|
editorState.context.setLineDash([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store the last valid rectangle drawn during the move event
|
function getEventCoordinates(e: any) {
|
||||||
editorState.lastInteractionRect = { x, y, width, height };
|
const rect = editorState.canvas.getBoundingClientRect();
|
||||||
|
const touch = e.touches ? e.touches[0] : e;
|
||||||
|
const scaleX = editorState.canvas.width / rect.width;
|
||||||
|
const scaleY = editorState.canvas.height / rect.height;
|
||||||
|
return {
|
||||||
|
x: (touch.clientX - rect.left) * scaleX,
|
||||||
|
y: (touch.clientY - rect.top) * scaleY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInteractionStart(e: any) {
|
||||||
|
e.preventDefault();
|
||||||
|
const coords = getEventCoordinates(e);
|
||||||
|
editorState.isDrawing = true;
|
||||||
|
editorState.startX = coords.x;
|
||||||
|
editorState.startY = coords.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInteractionMove(e: any) {
|
||||||
|
if (!editorState.isDrawing) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
redrawShapes();
|
||||||
|
const coords = getEventCoordinates(e);
|
||||||
|
|
||||||
|
const x = Math.min(editorState.startX, coords.x);
|
||||||
|
const y = Math.min(editorState.startY, coords.y);
|
||||||
|
const width = Math.abs(editorState.startX - coords.x);
|
||||||
|
const height = Math.abs(editorState.startY - coords.y);
|
||||||
|
|
||||||
|
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||||
|
editorState.context.lineWidth = 2;
|
||||||
|
editorState.context.setLineDash([8, 4]);
|
||||||
|
editorState.context.strokeRect(x, y, width, height);
|
||||||
|
editorState.context.setLineDash([]);
|
||||||
|
|
||||||
|
// Store the last valid rectangle drawn during the move event
|
||||||
|
editorState.lastInteractionRect = { x, y, width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInteractionEnd() {
|
function handleInteractionEnd() {
|
||||||
if (!editorState.isDrawing) return;
|
if (!editorState.isDrawing) return;
|
||||||
editorState.isDrawing = false;
|
editorState.isDrawing = false;
|
||||||
|
|
||||||
const finalRect = editorState.lastInteractionRect;
|
const finalRect = editorState.lastInteractionRect;
|
||||||
|
|
||||||
if (!finalRect || finalRect.width < 5 || finalRect.height < 5) {
|
if (!finalRect || finalRect.width < 5 || finalRect.height < 5) {
|
||||||
redrawShapes(); // Redraw to clear any invalid, tiny box
|
redrawShapes(); // Redraw to clear any invalid, tiny box
|
||||||
editorState.lastInteractionRect = null;
|
editorState.lastInteractionRect = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
redrawShapes();
|
redrawShapes();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupCanvasEditor(toolId: any) {
|
export async function setupCanvasEditor(toolId: any) {
|
||||||
editorState.canvas = document.getElementById('canvas-editor');
|
editorState.canvas = document.getElementById('canvas-editor');
|
||||||
if (!editorState.canvas) return;
|
if (!editorState.canvas) return;
|
||||||
editorState.container = document.getElementById('canvas-container');
|
editorState.container = document.getElementById('canvas-container');
|
||||||
editorState.context = editorState.canvas.getContext('2d');
|
editorState.context = editorState.canvas.getContext('2d');
|
||||||
|
|
||||||
const pageNav = document.getElementById('page-nav');
|
const pageNav = document.getElementById('page-nav');
|
||||||
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'.
|
||||||
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
|
|
||||||
editorState.cropBoxes = {};
|
editorState.cropBoxes = {};
|
||||||
editorState.currentPageNum = 1;
|
editorState.currentPageNum = 1;
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
|
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
|
||||||
editorState.scale = 'fit';
|
editorState.scale = 'fit';
|
||||||
|
|
||||||
pageNav.textContent = '';
|
pageNav.textContent = '';
|
||||||
|
|
||||||
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 =
|
||||||
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
|
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||||
|
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
|
||||||
|
|
||||||
const pageInfo = document.createElement('span');
|
const pageInfo = document.createElement('span');
|
||||||
pageInfo.className = 'text-white font-medium';
|
pageInfo.className = 'text-white font-medium';
|
||||||
|
|
||||||
const currentPageDisplay = document.createElement('span');
|
const currentPageDisplay = document.createElement('span');
|
||||||
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 =
|
||||||
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||||
|
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
||||||
|
|
||||||
pageNav.append(prevButton, pageInfo, nextButton);
|
pageNav.append(prevButton, pageInfo, nextButton);
|
||||||
|
|
||||||
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', () => {
|
});
|
||||||
if (editorState.currentPageNum < editorState.pdf.numPages) queueRenderPage(editorState.currentPageNum + 1);
|
document.getElementById('next-page').addEventListener('click', () => {
|
||||||
});
|
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
|
||||||
const newCanvas = editorState.canvas.cloneNode(true);
|
const newCanvas = editorState.canvas.cloneNode(true);
|
||||||
editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas);
|
editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas);
|
||||||
editorState.canvas = newCanvas;
|
editorState.canvas = newCanvas;
|
||||||
editorState.context = newCanvas.getContext('2d');
|
editorState.context = newCanvas.getContext('2d');
|
||||||
|
|
||||||
// Mouse Events
|
// Mouse Events
|
||||||
editorState.canvas.addEventListener('mousedown', handleInteractionStart);
|
editorState.canvas.addEventListener('mousedown', handleInteractionStart);
|
||||||
editorState.canvas.addEventListener('mousemove', handleInteractionMove);
|
editorState.canvas.addEventListener('mousemove', handleInteractionMove);
|
||||||
editorState.canvas.addEventListener('mouseup', handleInteractionEnd);
|
editorState.canvas.addEventListener('mouseup', handleInteractionEnd);
|
||||||
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('touchend', handleInteractionEnd);
|
});
|
||||||
|
editorState.canvas.addEventListener('touchmove', handleInteractionMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
||||||
|
|
||||||
if (toolId === 'crop') {
|
if (toolId === 'crop') {
|
||||||
document.getElementById('zoom-in-btn').onclick = () => {
|
document.getElementById('zoom-in-btn').onclick = () => {
|
||||||
editorState.scale += 0.25;
|
editorState.scale += 0.25;
|
||||||
renderPage(editorState.currentPageNum);
|
renderPage(editorState.currentPageNum);
|
||||||
};
|
};
|
||||||
document.getElementById('zoom-out-btn').onclick = () => {
|
document.getElementById('zoom-out-btn').onclick = () => {
|
||||||
if (editorState.scale > 0.25) {
|
if (editorState.scale > 0.25) {
|
||||||
editorState.scale -= 0.25;
|
editorState.scale -= 0.25;
|
||||||
renderPage(editorState.currentPageNum);
|
renderPage(editorState.currentPageNum);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.getElementById('fit-page-btn').onclick = async () => {
|
document.getElementById('fit-page-btn').onclick = async () => {
|
||||||
const page = await editorState.pdf.getPage(editorState.currentPageNum);
|
const page = await editorState.pdf.getPage(editorState.currentPageNum);
|
||||||
editorState.scale = calculateFitScale(page);
|
editorState.scale = calculateFitScale(page);
|
||||||
renderPage(editorState.currentPageNum);
|
renderPage(editorState.currentPageNum);
|
||||||
};
|
};
|
||||||
document.getElementById('clear-crop-btn').onclick = () => {
|
document.getElementById('clear-crop-btn').onclick = () => {
|
||||||
delete editorState.cropBoxes[editorState.currentPageNum - 1];
|
delete editorState.cropBoxes[editorState.currentPageNum - 1];
|
||||||
redrawShapes();
|
redrawShapes();
|
||||||
};
|
};
|
||||||
document.getElementById('clear-all-crops-btn').onclick = () => {
|
document.getElementById('clear-all-crops-btn').onclick = () => {
|
||||||
editorState.cropBoxes = {};
|
editorState.cropBoxes = {};
|
||||||
redrawShapes();
|
redrawShapes();
|
||||||
};
|
};
|
||||||
|
|
||||||
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(
|
||||||
return;
|
'No Area Selected',
|
||||||
}
|
'Please draw a rectangle on at least one page to select the crop area.'
|
||||||
const success = await toolLogic['crop-pdf'].process(editorState.cropBoxes);
|
);
|
||||||
if (success) {
|
return;
|
||||||
showAlert('Success!', 'Your PDF has been cropped and the download has started.');
|
}
|
||||||
}
|
const success = await toolLogic['crop-pdf'].process(
|
||||||
};
|
editorState.cropBoxes
|
||||||
}
|
);
|
||||||
|
if (success) {
|
||||||
|
showAlert(
|
||||||
|
'Success!',
|
||||||
|
'Your PDF has been cropped and the download has started.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
queueRenderPage(1);
|
queueRenderPage(1);
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,65 @@
|
|||||||
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',
|
||||||
];
|
];
|
||||||
|
|
||||||
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',
|
||||||
};
|
};
|
||||||
+438
-102
@@ -1,105 +1,441 @@
|
|||||||
// This file centralizes the definition of all available tools, organized by category.
|
// This file centralizes the definition of all available tools, organized by category.
|
||||||
export const categories = [
|
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.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Edit & Annotate',
|
id: 'compress',
|
||||||
tools: [
|
name: 'Compress PDF',
|
||||||
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.' },
|
icon: 'zap',
|
||||||
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
subtitle: 'Reduce the file size of 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-header-footer', name: 'Header & Footer', icon: 'pilcrow', subtitle: 'Add text to the top and bottom of pages.' },
|
id: 'edit',
|
||||||
{ id: 'invert-colors', name: 'Invert Colors', icon: 'contrast', subtitle: 'Create a "dark mode" version of your PDF.' },
|
name: 'PDF Editor',
|
||||||
{ id: 'change-background-color', name: 'Background Color', icon: 'palette', subtitle: 'Change the background color of your PDF.' },
|
icon: 'pocket-knife',
|
||||||
{ id: 'change-text-color', name: 'Change Text Color', icon: 'type', subtitle: 'Change the color of text in your PDF.' },
|
subtitle:
|
||||||
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
|
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs',
|
||||||
{ 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: 'jpg-to-pdf',
|
||||||
]
|
name: 'JPG to PDF',
|
||||||
},
|
icon: 'image-up',
|
||||||
{
|
subtitle: 'Create a PDF from one or more JPG images.',
|
||||||
name: 'Convert to PDF',
|
},
|
||||||
tools: [
|
{
|
||||||
{ id: 'image-to-pdf', name: 'Image to PDF', icon: 'images', subtitle: 'Combine various images into one PDF.' },
|
id: 'sign-pdf',
|
||||||
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
|
name: 'Sign PDF',
|
||||||
{ id: 'png-to-pdf', name: 'PNG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more PNG images.' },
|
icon: 'pen-tool',
|
||||||
{ id: 'webp-to-pdf', name: 'WebP to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more WebP images.' },
|
subtitle: 'Draw, type, or upload your signature.',
|
||||||
{ 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: 'cropper',
|
||||||
{ id: 'tiff-to-pdf', name: 'TIFF to PDF', icon: 'layers', subtitle: 'Create a PDF from one or more TIFF images.' },
|
name: 'Crop PDF',
|
||||||
{ id: 'txt-to-pdf', name: 'Text to PDF', icon: 'file-pen', subtitle: 'Convert a plain text file into a PDF.' },
|
icon: 'crop',
|
||||||
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
|
subtitle: 'Trim the margins of every page in your 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: 'extract-pages',
|
||||||
},
|
name: 'Extract Pages',
|
||||||
{
|
icon: 'ungroup',
|
||||||
name: 'Convert from PDF',
|
subtitle: 'Save a selection of pages as new files.',
|
||||||
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: 'duplicate-organize',
|
||||||
{ id: 'pdf-to-webp', name: 'PDF to WebP', icon: 'file-image', subtitle: 'Convert each PDF page into a WebP image.' },
|
name: 'Duplicate & Organize',
|
||||||
{ id: 'pdf-to-bmp', name: 'PDF to BMP', icon: 'file-image', subtitle: 'Convert each PDF page into a BMP image.' },
|
icon: 'files',
|
||||||
{ id: 'pdf-to-tiff', name: 'PDF to TIFF', icon: 'file-image', subtitle: 'Convert each PDF page into a TIFF image.' },
|
subtitle: 'Duplicate, reorder, and delete pages.',
|
||||||
{ 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: 'delete-pages',
|
||||||
},
|
name: 'Delete Pages',
|
||||||
{
|
icon: 'trash-2',
|
||||||
name: 'Organize & Manage',
|
subtitle: 'Remove specific pages from your document.',
|
||||||
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: '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.' },
|
name: 'Edit & Annotate',
|
||||||
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
|
tools: [
|
||||||
{ 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: 'edit',
|
||||||
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
|
name: 'PDF Editor',
|
||||||
{ id: 'add-blank-page', name: 'Add Blank Page', icon: 'file-plus-2', subtitle: 'Insert an empty page anywhere in your PDF.' },
|
icon: 'pocket-knife',
|
||||||
{ id: 'reverse-pages', name: 'Reverse Pages', icon: 'arrow-down-z-a', subtitle: 'Flip the order of all pages in your document.' },
|
subtitle:
|
||||||
{ id: 'rotate', name: 'Rotate PDF', icon: 'rotate-cw', subtitle: 'Turn pages in 90-degree increments.' },
|
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.',
|
||||||
{ 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: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
||||||
{ 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: 'add-page-numbers',
|
||||||
{ id: 'pdf-to-zip', name: 'PDFs to ZIP', icon: 'stretch-horizontal', subtitle: 'Package multiple PDF files into a ZIP archive.' },
|
name: 'Page Numbers',
|
||||||
{ id: 'compare-pdfs', name: 'Compare PDFs', icon: 'git-compare', subtitle: 'Compare two PDFs side by side.' },
|
icon: 'list-ordered',
|
||||||
{ id: 'posterize', name: 'Posterize PDF', icon: 'layout-grid', subtitle: 'Split a large page into multiple smaller pages.' },
|
subtitle: 'Insert page numbers into your document.',
|
||||||
]
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 'add-watermark',
|
||||||
name: 'Optimize & Repair',
|
name: 'Add Watermark',
|
||||||
tools: [
|
icon: 'droplets',
|
||||||
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
|
subtitle: 'Stamp text or an image over your PDF pages.',
|
||||||
{ id: 'fix-dimensions', name: 'Fix Page Size', icon: 'ruler-dimension-line', subtitle: 'Standardize all pages to a uniform size.' },
|
},
|
||||||
{ id: 'page-dimensions', name: 'Page Dimensions', icon: 'ruler', subtitle: 'Analyze page size, orientation, and units.' },
|
{
|
||||||
]
|
id: 'add-header-footer',
|
||||||
},
|
name: 'Header & Footer',
|
||||||
{
|
icon: 'pilcrow',
|
||||||
name: 'Secure PDF',
|
subtitle: 'Add text to the top and bottom of pages.',
|
||||||
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: 'invert-colors',
|
||||||
{ id: 'flatten', name: 'Flatten PDF', icon: 'layers', subtitle: 'Make form fields and annotations non-editable.' },
|
name: 'Invert Colors',
|
||||||
{ id: 'remove-metadata', name: 'Remove Metadata', icon: 'file-x', subtitle: 'Strip hidden data from your PDF.' },
|
icon: 'contrast',
|
||||||
{ id: 'change-permissions', name: 'Change Permissions', icon: 'shield-check', subtitle: 'Set or change user permissions on a PDF.' },
|
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',
|
||||||
|
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: '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: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
|
||||||
|
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Convert from PDF',
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
id: 'pdf-to-jpg',
|
||||||
|
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-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.' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Organize & Manage',
|
||||||
|
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: 'alternate-merge',
|
||||||
|
name: 'Alternate & Mix Pages',
|
||||||
|
icon: 'shuffle',
|
||||||
|
subtitle: 'Combine PDFs by alternating pages from each.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: 'layout-grid',
|
||||||
|
subtitle: 'Split a large page into multiple smaller pages.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Optimize & Repair',
|
||||||
|
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: 'linearize',
|
||||||
|
name: 'Linearize PDF',
|
||||||
|
icon: 'zap',
|
||||||
|
subtitle: 'Optimize PDF for fast web viewing.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-dimensions',
|
||||||
|
name: 'Page Dimensions',
|
||||||
|
icon: 'ruler',
|
||||||
|
subtitle: 'Analyze page size, orientation, and units.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Secure PDF',
|
||||||
|
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: '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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
+495
-358
@@ -1,5 +1,12 @@
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js';
|
import {
|
||||||
|
showLoader,
|
||||||
|
hideLoader,
|
||||||
|
showAlert,
|
||||||
|
renderPageThumbnails,
|
||||||
|
renderFileDisplay,
|
||||||
|
switchView,
|
||||||
|
} from '../ui.js';
|
||||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { 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';
|
||||||
@@ -7,350 +14,473 @@ import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.j
|
|||||||
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) {
|
||||||
showLoader('Loading PDF...');
|
showLoader('Loading PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||||
ignoreEncryption: true
|
ignoreEncryption: true,
|
||||||
});
|
});
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
if (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 &&
|
||||||
switchView('grid');
|
toolId !== 'decrypt' &&
|
||||||
return;
|
toolId !== 'change-permissions'
|
||||||
}
|
) {
|
||||||
|
showAlert(
|
||||||
const optionsDiv = document.querySelector('[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]');
|
'Protected PDF',
|
||||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.'
|
||||||
|
);
|
||||||
const processBtn = document.getElementById('process-btn');
|
switchView('grid');
|
||||||
if (processBtn) {
|
return;
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
|
||||||
processBtn.classList.remove('hidden');
|
|
||||||
const logic = toolLogic[toolId];
|
|
||||||
if (logic) {
|
|
||||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
|
||||||
processBtn.onclick = func;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['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') {
|
|
||||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
|
||||||
|
|
||||||
if (toolId === 'rotate') {
|
|
||||||
const rotateAllControls = document.getElementById('rotate-all-controls');
|
|
||||||
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
|
||||||
const rotateAllRightBtn = document.getElementById('rotate-all-right-btn');
|
|
||||||
|
|
||||||
rotateAllControls.classList.remove('hidden');
|
|
||||||
createIcons({ icons });
|
|
||||||
|
|
||||||
const rotateAll = (direction) => {
|
|
||||||
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
|
||||||
const currentRotation = parseInt((item as HTMLElement).dataset.rotation || '0');
|
|
||||||
const newRotation = (currentRotation + (direction * 90) + 360) % 360;
|
|
||||||
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
|
||||||
const thumbnail = item.querySelector('canvas, img');
|
|
||||||
if (thumbnail) {
|
|
||||||
(thumbnail as HTMLElement).style.transform = `rotate(${newRotation}deg)`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
rotateAllLeftBtn.onclick = () => rotateAll(-1);
|
|
||||||
rotateAllRightBtn.onclick = () => rotateAll(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolId === 'duplicate-organize') {
|
|
||||||
await renderDuplicateOrganizeThumbnails();
|
|
||||||
}
|
|
||||||
if (['crop', 'redact'].includes(toolId)) {
|
|
||||||
await setupCanvasEditor(toolId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolId === 'view-metadata') {
|
|
||||||
const resultsDiv = document.getElementById('metadata-results');
|
|
||||||
showLoader('Analyzing full PDF metadata...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes as ArrayBuffer }).promise;
|
|
||||||
const [metadata, fieldObjects] = await Promise.all([
|
|
||||||
pdfjsDoc.getMetadata(),
|
|
||||||
pdfjsDoc.getFieldObjects()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { info, metadata: rawXmpString } = metadata;
|
|
||||||
|
|
||||||
resultsDiv.textContent = ''; // Clear safely
|
|
||||||
|
|
||||||
const createSection = (title) => {
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
wrapper.className = 'mb-4';
|
|
||||||
const h3 = document.createElement('h3');
|
|
||||||
h3.className = 'text-lg font-semibold text-white mb-2';
|
|
||||||
h3.textContent = title;
|
|
||||||
const ul = document.createElement('ul');
|
|
||||||
ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
|
||||||
wrapper.append(h3, ul);
|
|
||||||
return { wrapper, ul };
|
|
||||||
};
|
|
||||||
|
|
||||||
const createListItem = (key, value) => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.className = 'flex flex-col sm:flex-row';
|
|
||||||
const strong = document.createElement('strong');
|
|
||||||
strong.className = 'w-40 flex-shrink-0 text-gray-400';
|
|
||||||
strong.textContent = key;
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'flex-grow text-white break-all';
|
|
||||||
div.textContent = value;
|
|
||||||
li.append(strong, div);
|
|
||||||
return li;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsePdfDate = (pdfDate) => {
|
|
||||||
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) return pdfDate;
|
|
||||||
try {
|
|
||||||
const year = pdfDate.substring(2, 6);
|
|
||||||
const month = pdfDate.substring(6, 8);
|
|
||||||
const day = pdfDate.substring(8, 10);
|
|
||||||
const hour = pdfDate.substring(10, 12);
|
|
||||||
const minute = pdfDate.substring(12, 14);
|
|
||||||
const second = pdfDate.substring(14, 16);
|
|
||||||
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
|
|
||||||
} catch { return pdfDate; }
|
|
||||||
};
|
|
||||||
|
|
||||||
const infoSection = createSection('Info Dictionary');
|
|
||||||
if (info && Object.keys(info).length > 0) {
|
|
||||||
for (const key in info) {
|
|
||||||
let value = info[key] || '- Not Set -';
|
|
||||||
if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
|
|
||||||
value = parsePdfDate(value);
|
|
||||||
}
|
|
||||||
infoSection.ul.appendChild(createListItem(key, String(value)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
|
||||||
}
|
|
||||||
resultsDiv.appendChild(infoSection.wrapper);
|
|
||||||
|
|
||||||
const fieldsSection = createSection('Interactive Form Fields');
|
|
||||||
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
|
||||||
for (const fieldName in fieldObjects) {
|
|
||||||
const field = fieldObjects[fieldName][0];
|
|
||||||
const value = (field as any).fieldValue || '- Not Set -';
|
|
||||||
fieldsSection.ul.appendChild(createListItem(fieldName, String(value)));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
|
||||||
}
|
|
||||||
resultsDiv.appendChild(fieldsSection.wrapper);
|
|
||||||
|
|
||||||
const xmpSection = createSection('XMP Metadata (Raw XML)');
|
|
||||||
const xmpContainer = document.createElement('div');
|
|
||||||
xmpContainer.className = 'bg-gray-900 p-4 rounded-lg border border-gray-700';
|
|
||||||
if (rawXmpString) {
|
|
||||||
const pre = document.createElement('pre');
|
|
||||||
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
|
||||||
pre.textContent = String(rawXmpString);
|
|
||||||
xmpContainer.appendChild(pre);
|
|
||||||
} else {
|
|
||||||
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
|
||||||
}
|
|
||||||
xmpSection.wrapper.appendChild(xmpContainer);
|
|
||||||
resultsDiv.appendChild(xmpSection.wrapper);
|
|
||||||
|
|
||||||
resultsDiv.classList.remove('hidden');
|
|
||||||
|
|
||||||
} catch (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.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolId === 'edit-metadata') {
|
|
||||||
const form = document.getElementById('metadata-form');
|
|
||||||
const container = document.getElementById('custom-metadata-container');
|
|
||||||
const addBtn = document.getElementById('add-custom-meta-btn');
|
|
||||||
|
|
||||||
const formatDateForInput = (date) => {
|
|
||||||
if (!date) return '';
|
|
||||||
const pad = (num) => num.toString().padStart(2, '0');
|
|
||||||
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-author') as HTMLInputElement).value = state.pdfDoc.getAuthor() || '';
|
|
||||||
(document.getElementById('meta-subject') as HTMLInputElement).value = state.pdfDoc.getSubject() || '';
|
|
||||||
(document.getElementById('meta-keywords') as HTMLInputElement).value = 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 = () => {
|
|
||||||
const fieldWrapper = document.createElement('div');
|
|
||||||
fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper';
|
|
||||||
|
|
||||||
const keyInput = document.createElement('input');
|
|
||||||
keyInput.type = 'text';
|
|
||||||
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';
|
|
||||||
|
|
||||||
const valueInput = document.createElement('input');
|
|
||||||
valueInput.type = 'text';
|
|
||||||
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';
|
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
|
||||||
removeBtn.type = 'button';
|
|
||||||
removeBtn.className = 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
|
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
|
||||||
removeBtn.addEventListener('click', () => fieldWrapper.remove());
|
|
||||||
|
|
||||||
fieldWrapper.append(keyInput, valueInput, removeBtn);
|
|
||||||
container.appendChild(fieldWrapper);
|
|
||||||
createIcons({ icons });
|
|
||||||
};
|
|
||||||
|
|
||||||
form.classList.remove('hidden');
|
|
||||||
createIcons({ icons });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolId === 'cropper') {
|
|
||||||
document.getElementById('cropper-ui-container').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolId === 'page-dimensions') {
|
|
||||||
toolLogic['page-dimensions']();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
|
||||||
toolLogic[toolId].setup();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', 'Could not load PDF. The file may be invalid, corrupted, or password-protected.');
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function handleMultiFileUpload(toolId) {
|
const optionsDiv = document.querySelector(
|
||||||
|
'[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]'
|
||||||
|
);
|
||||||
|
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
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;
|
||||||
const logic = toolLogic[toolId];
|
processBtn.classList.remove('hidden');
|
||||||
if (logic) {
|
const logic = toolLogic[toolId];
|
||||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
if (logic) {
|
||||||
processBtn.onclick = func;
|
const func =
|
||||||
}
|
typeof logic.process === 'function' ? logic.process : logic;
|
||||||
|
processBtn.onclick = func;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'merge') {
|
if (
|
||||||
toolLogic.merge.setup();
|
[
|
||||||
} else if (toolId === 'image-to-pdf') {
|
'split',
|
||||||
const imageList = document.getElementById('image-list');
|
'delete-pages',
|
||||||
imageList.textContent = ''; // Clear safely
|
'add-blank-page',
|
||||||
|
'extract-pages',
|
||||||
|
'add-header-footer',
|
||||||
|
].includes(toolId)
|
||||||
|
) {
|
||||||
|
document.getElementById('total-pages').textContent = state.pdfDoc
|
||||||
|
.getPageCount()
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
state.files.forEach(file => {
|
if (toolId === 'organize' || toolId === 'rotate') {
|
||||||
const url = URL.createObjectURL(file);
|
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||||
const li = document.createElement('li');
|
|
||||||
li.className = "relative group cursor-move";
|
|
||||||
li.dataset.fileName = file.name;
|
|
||||||
|
|
||||||
const img = document.createElement('img');
|
if (toolId === 'rotate') {
|
||||||
img.src = url;
|
const rotateAllControls = document.getElementById(
|
||||||
img.className = "w-full h-full object-cover rounded-md border-2 border-gray-600";
|
'rotate-all-controls'
|
||||||
|
);
|
||||||
|
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
||||||
|
const rotateAllRightBtn = document.getElementById(
|
||||||
|
'rotate-all-right-btn'
|
||||||
|
);
|
||||||
|
|
||||||
const p = document.createElement('p');
|
rotateAllControls.classList.remove('hidden');
|
||||||
p.className = "absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1";
|
createIcons({ icons });
|
||||||
p.textContent = file.name; // Safe insertion
|
|
||||||
|
|
||||||
li.append(img, p);
|
const rotateAll = (direction) => {
|
||||||
imageList.appendChild(li);
|
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
||||||
|
const currentRotation = parseInt(
|
||||||
|
(item as HTMLElement).dataset.rotation || '0'
|
||||||
|
);
|
||||||
|
const newRotation = (currentRotation + direction * 90 + 360) % 360;
|
||||||
|
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||||
|
const thumbnail = item.querySelector('canvas, img');
|
||||||
|
if (thumbnail) {
|
||||||
|
(thumbnail as HTMLElement).style.transform =
|
||||||
|
`rotate(${newRotation}deg)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
rotateAllLeftBtn.onclick = () => rotateAll(-1);
|
||||||
|
rotateAllRightBtn.onclick = () => rotateAll(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolId === 'duplicate-organize') {
|
||||||
|
await renderDuplicateOrganizeThumbnails();
|
||||||
|
}
|
||||||
|
if (['crop', 'redact'].includes(toolId)) {
|
||||||
|
await setupCanvasEditor(toolId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolId === 'view-metadata') {
|
||||||
|
const resultsDiv = document.getElementById('metadata-results');
|
||||||
|
showLoader('Analyzing full PDF metadata...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
||||||
|
const pdfjsDoc = await pdfjsLib.getDocument({
|
||||||
|
data: pdfBytes as ArrayBuffer,
|
||||||
|
}).promise;
|
||||||
|
const [metadata, fieldObjects] = await Promise.all([
|
||||||
|
pdfjsDoc.getMetadata(),
|
||||||
|
pdfjsDoc.getFieldObjects(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { info, metadata: rawXmpString } = metadata;
|
||||||
|
|
||||||
|
resultsDiv.textContent = ''; // Clear safely
|
||||||
|
|
||||||
|
const createSection = (title) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'mb-4';
|
||||||
|
const h3 = document.createElement('h3');
|
||||||
|
h3.className = 'text-lg font-semibold text-white mb-2';
|
||||||
|
h3.textContent = title;
|
||||||
|
const ul = document.createElement('ul');
|
||||||
|
ul.className =
|
||||||
|
'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||||
|
wrapper.append(h3, ul);
|
||||||
|
return { wrapper, ul };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createListItem = (key, value) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'flex flex-col sm:flex-row';
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.className = 'w-40 flex-shrink-0 text-gray-400';
|
||||||
|
strong.textContent = key;
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex-grow text-white break-all';
|
||||||
|
div.textContent = value;
|
||||||
|
li.append(strong, div);
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsePdfDate = (pdfDate) => {
|
||||||
|
if (
|
||||||
|
!pdfDate ||
|
||||||
|
typeof pdfDate !== 'string' ||
|
||||||
|
!pdfDate.startsWith('D:')
|
||||||
|
)
|
||||||
|
return pdfDate;
|
||||||
|
try {
|
||||||
|
const year = pdfDate.substring(2, 6);
|
||||||
|
const month = pdfDate.substring(6, 8);
|
||||||
|
const day = pdfDate.substring(8, 10);
|
||||||
|
const hour = pdfDate.substring(10, 12);
|
||||||
|
const minute = pdfDate.substring(12, 14);
|
||||||
|
const second = pdfDate.substring(14, 16);
|
||||||
|
return new Date(
|
||||||
|
`${year}-${month}-${day}T${hour}:${minute}:${second}Z`
|
||||||
|
).toLocaleString();
|
||||||
|
} catch {
|
||||||
|
return pdfDate;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const infoSection = createSection('Info Dictionary');
|
||||||
|
if (info && Object.keys(info).length > 0) {
|
||||||
|
for (const key in info) {
|
||||||
|
let value = info[key] || '- Not Set -';
|
||||||
|
if (
|
||||||
|
(key === 'CreationDate' || key === 'ModDate') &&
|
||||||
|
typeof value === 'string'
|
||||||
|
) {
|
||||||
|
value = parsePdfDate(value);
|
||||||
|
}
|
||||||
|
infoSection.ul.appendChild(createListItem(key, String(value)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||||
|
}
|
||||||
|
resultsDiv.appendChild(infoSection.wrapper);
|
||||||
|
|
||||||
|
const fieldsSection = createSection('Interactive Form Fields');
|
||||||
|
if (fieldObjects && Object.keys(fieldObjects).length > 0) {
|
||||||
|
for (const fieldName in fieldObjects) {
|
||||||
|
const field = fieldObjects[fieldName][0];
|
||||||
|
const value = (field as any).fieldValue || '- Not Set -';
|
||||||
|
fieldsSection.ul.appendChild(
|
||||||
|
createListItem(fieldName, String(value))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
||||||
|
}
|
||||||
|
resultsDiv.appendChild(fieldsSection.wrapper);
|
||||||
|
|
||||||
|
const xmpSection = createSection('XMP Metadata (Raw XML)');
|
||||||
|
const xmpContainer = document.createElement('div');
|
||||||
|
xmpContainer.className =
|
||||||
|
'bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||||
|
if (rawXmpString) {
|
||||||
|
const pre = document.createElement('pre');
|
||||||
|
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||||
|
pre.textContent = String(rawXmpString);
|
||||||
|
xmpContainer.appendChild(pre);
|
||||||
|
} else {
|
||||||
|
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
||||||
|
}
|
||||||
|
xmpSection.wrapper.appendChild(xmpContainer);
|
||||||
|
resultsDiv.appendChild(xmpSection.wrapper);
|
||||||
|
|
||||||
|
resultsDiv.classList.remove('hidden');
|
||||||
|
} catch (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.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolId === 'edit-metadata') {
|
||||||
|
const form = document.getElementById('metadata-form');
|
||||||
|
const container = document.getElementById('custom-metadata-container');
|
||||||
|
const addBtn = document.getElementById('add-custom-meta-btn');
|
||||||
|
|
||||||
|
const formatDateForInput = (date) => {
|
||||||
|
if (!date) return '';
|
||||||
|
const pad = (num) => num.toString().padStart(2, '0');
|
||||||
|
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-author') as HTMLInputElement).value =
|
||||||
|
state.pdfDoc.getAuthor() || '';
|
||||||
|
(document.getElementById('meta-subject') as HTMLInputElement).value =
|
||||||
|
state.pdfDoc.getSubject() || '';
|
||||||
|
(document.getElementById('meta-keywords') as HTMLInputElement).value =
|
||||||
|
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 = () => {
|
||||||
|
const fieldWrapper = document.createElement('div');
|
||||||
|
fieldWrapper.className = 'flex items-center gap-2 custom-field-wrapper';
|
||||||
|
|
||||||
|
const keyInput = document.createElement('input');
|
||||||
|
keyInput.type = 'text';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const valueInput = document.createElement('input');
|
||||||
|
valueInput.type = 'text';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.type = 'button';
|
||||||
|
removeBtn.className =
|
||||||
|
'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
|
||||||
|
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
||||||
|
removeBtn.addEventListener('click', () => fieldWrapper.remove());
|
||||||
|
|
||||||
|
fieldWrapper.append(keyInput, valueInput, removeBtn);
|
||||||
|
container.appendChild(fieldWrapper);
|
||||||
|
createIcons({ icons });
|
||||||
|
};
|
||||||
|
|
||||||
|
form.classList.remove('hidden');
|
||||||
|
createIcons({ icons });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolId === 'cropper') {
|
||||||
|
document
|
||||||
|
.getElementById('cropper-ui-container')
|
||||||
|
.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolId === 'page-dimensions') {
|
||||||
|
toolLogic['page-dimensions']();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
||||||
|
toolLogic[toolId].setup();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not load PDF. The file may be invalid, corrupted, or password-protected.'
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
});
|
});
|
||||||
|
|
||||||
Sortable.create(imageList);
|
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');
|
||||||
|
if (processBtn) {
|
||||||
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
|
const logic = toolLogic[toolId];
|
||||||
|
if (logic) {
|
||||||
|
const func = typeof logic.process === 'function' ? logic.process : logic;
|
||||||
|
processBtn.onclick = func;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolId === 'merge') {
|
||||||
|
toolLogic.merge.setup();
|
||||||
|
} else if (toolId === 'alternate-merge') {
|
||||||
|
toolLogic['alternate-merge'].setup();
|
||||||
|
} else if (toolId === 'image-to-pdf') {
|
||||||
|
const imageList = document.getElementById('image-list');
|
||||||
|
imageList.textContent = ''; // Clear safely
|
||||||
|
|
||||||
|
state.files.forEach((file) => {
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'relative group cursor-move';
|
||||||
|
li.dataset.fileName = file.name;
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = url;
|
||||||
|
img.className =
|
||||||
|
'w-full h-full object-cover rounded-md border-2 border-gray-600';
|
||||||
|
|
||||||
|
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.textContent = file.name; // Safe insertion
|
||||||
|
|
||||||
|
li.append(img, p);
|
||||||
|
imageList.appendChild(li);
|
||||||
|
});
|
||||||
|
|
||||||
|
Sortable.create(imageList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupFileInputHandler(toolId) {
|
export function setupFileInputHandler(toolId) {
|
||||||
const fileInput = document.getElementById('file-input');
|
const fileInput = document.getElementById('file-input');
|
||||||
const isMultiFileTool = multiFileTools.includes(toolId);
|
const isMultiFileTool = multiFileTools.includes(toolId);
|
||||||
let isFirstUpload = true;
|
let isFirstUpload = true;
|
||||||
|
|
||||||
const processFiles = async (newFiles) => {
|
const processFiles = async (newFiles) => {
|
||||||
if (newFiles.length === 0) return;
|
if (newFiles.length === 0) return;
|
||||||
|
|
||||||
if (!isMultiFileTool || isFirstUpload) {
|
if (!isMultiFileTool || isFirstUpload) {
|
||||||
state.files = newFiles;
|
state.files = newFiles;
|
||||||
} else {
|
} else {
|
||||||
state.files = [...state.files, ...newFiles];
|
state.files = [...state.files, ...newFiles];
|
||||||
}
|
}
|
||||||
isFirstUpload = false;
|
isFirstUpload = false;
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) {
|
if (fileDisplayArea) {
|
||||||
renderFileDisplay(fileDisplayArea, state.files);
|
renderFileDisplay(fileDisplayArea, state.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
if (fileControls) {
|
if (fileControls) {
|
||||||
fileControls.classList.remove('hidden');
|
fileControls.classList.remove('hidden');
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
}
|
}
|
||||||
|
|
||||||
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 =
|
||||||
const optionsDiv = document.getElementById(optionsDivId);
|
toolId === 'change-permissions'
|
||||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
? 'permissions-options'
|
||||||
const processBtn = document.getElementById('process-btn');
|
: `${toolId}-options`;
|
||||||
if (processBtn) {
|
const optionsDiv = document.getElementById(optionsDivId);
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||||
processBtn.onclick = () => {
|
const processBtn = document.getElementById('process-btn');
|
||||||
const logic = toolLogic[toolId];
|
if (processBtn) {
|
||||||
if (logic) {
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
processBtn.onclick = () => {
|
||||||
func();
|
const logic = toolLogic[toolId];
|
||||||
}
|
if (logic) {
|
||||||
};
|
const func =
|
||||||
}
|
typeof logic.process === 'function' ? logic.process : logic;
|
||||||
} else if (toolId === 'edit') {
|
func();
|
||||||
const file = state.files[0];
|
}
|
||||||
if (!file) return;
|
};
|
||||||
|
}
|
||||||
|
} else if (toolId === 'edit') {
|
||||||
|
const file = state.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
|
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
|
||||||
const pdfContainer = document.getElementById('embed-pdf-container');
|
const pdfContainer = document.getElementById('embed-pdf-container');
|
||||||
|
|
||||||
pdfContainer.textContent = ''; // Clear safely
|
pdfContainer.textContent = ''; // Clear safely
|
||||||
|
|
||||||
if (state.currentPdfUrl) {
|
if (state.currentPdfUrl) {
|
||||||
URL.revokeObjectURL(state.currentPdfUrl);
|
URL.revokeObjectURL(state.currentPdfUrl);
|
||||||
}
|
}
|
||||||
pdfWrapper.classList.remove('hidden');
|
pdfWrapper.classList.remove('hidden');
|
||||||
const fileURL = URL.createObjectURL(file);
|
const fileURL = URL.createObjectURL(file);
|
||||||
state.currentPdfUrl = fileURL;
|
state.currentPdfUrl = fileURL;
|
||||||
|
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.type = 'module';
|
script.type = 'module';
|
||||||
script.textContent = `
|
script.textContent = `
|
||||||
import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js';
|
import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js';
|
||||||
EmbedPDF.init({
|
EmbedPDF.init({
|
||||||
type: 'container',
|
type: 'container',
|
||||||
@@ -359,55 +489,62 @@ export function setupFileInputHandler(toolId) {
|
|||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
});
|
});
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|
||||||
const backBtn = document.getElementById('back-to-grid');
|
const backBtn = document.getElementById('back-to-grid');
|
||||||
const urlRevoker = () => {
|
const urlRevoker = () => {
|
||||||
URL.revokeObjectURL(fileURL);
|
URL.revokeObjectURL(fileURL);
|
||||||
state.currentPdfUrl = null;
|
state.currentPdfUrl = null;
|
||||||
backBtn.removeEventListener('click', urlRevoker);
|
backBtn.removeEventListener('click', urlRevoker);
|
||||||
};
|
};
|
||||||
backBtn.addEventListener('click', urlRevoker);
|
backBtn.addEventListener('click', urlRevoker);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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');
|
||||||
if (addMoreBtn) {
|
if (addMoreBtn) {
|
||||||
addMoreBtn.addEventListener('click', () => fileInput.click());
|
addMoreBtn.addEventListener('click', () => fileInput.click());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setupClearButton = () => {
|
const setupClearButton = () => {
|
||||||
const clearBtn = document.getElementById('clear-files-btn');
|
const clearBtn = document.getElementById('clear-files-btn');
|
||||||
if (clearBtn) {
|
if (clearBtn) {
|
||||||
clearBtn.addEventListener('click', () => {
|
clearBtn.addEventListener('click', () => {
|
||||||
state.files = [];
|
state.files = [];
|
||||||
isFirstUpload = true;
|
isFirstUpload = true;
|
||||||
(fileInput as HTMLInputElement).value = '';
|
(fileInput as HTMLInputElement).value = '';
|
||||||
|
|
||||||
const fileDisplayArea = document.getElementById('file-display-area');
|
const fileDisplayArea = document.getElementById('file-display-area');
|
||||||
if (fileDisplayArea) fileDisplayArea.textContent = '';
|
if (fileDisplayArea) fileDisplayArea.textContent = '';
|
||||||
|
|
||||||
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',
|
||||||
const el = document.getElementById(id);
|
'page-merge-preview',
|
||||||
if (el) el.textContent = '';
|
'image-list',
|
||||||
});
|
'alternate-file-list',
|
||||||
|
];
|
||||||
|
toolSpecificUI.forEach((id) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = '';
|
||||||
|
});
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) (processBtn as HTMLButtonElement).disabled = true;
|
if (processBtn) (processBtn as HTMLButtonElement).disabled = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setupAddMoreButton();
|
setupAddMoreButton();
|
||||||
setupClearButton();
|
setupClearButton();
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,35 +7,35 @@ import { createIcons, icons } from 'lucide';
|
|||||||
const SETUP_AFTER_UPLOAD = ['sign-pdf'];
|
const SETUP_AFTER_UPLOAD = ['sign-pdf'];
|
||||||
|
|
||||||
export function setupToolInterface(toolId: any) {
|
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;
|
||||||
dom.toolContent.innerHTML = toolTemplates[toolId]();
|
dom.toolContent.innerHTML = toolTemplates[toolId]();
|
||||||
createIcons({icons});
|
createIcons({ icons });
|
||||||
switchView('tool');
|
switchView('tool');
|
||||||
|
|
||||||
const fileInput = document.getElementById('file-input');
|
const fileInput = document.getElementById('file-input');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (!fileInput && processBtn) {
|
if (!fileInput && processBtn) {
|
||||||
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 (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
|
||||||
if (!SETUP_AFTER_UPLOAD.includes(toolId)) {
|
if (!SETUP_AFTER_UPLOAD.includes(toolId)) {
|
||||||
toolLogic[toolId].setup();
|
toolLogic[toolId].setup();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (fileInput) {
|
if (fileInput) {
|
||||||
setupFileInputHandler(toolId);
|
setupFileInputHandler(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;
|
||||||
|
}
|
||||||
@@ -4,48 +4,73 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
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;
|
||||||
if (pageNumberInput.trim() === '') {
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
showAlert('Invalid Input', 'Please enter a page number.');
|
const pageCountInput = document.getElementById('page-count').value;
|
||||||
return;
|
|
||||||
|
if (pageNumberInput.trim() === '') {
|
||||||
|
showAlert('Invalid Input', 'Please enter a page number.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageCountInput.trim() === '') {
|
||||||
|
showAlert('Invalid Input', 'Please enter the number of pages to insert.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const position = parseInt(pageNumberInput);
|
||||||
|
const pageCount = parseInt(pageCountInput);
|
||||||
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
|
if (isNaN(position) || position < 0 || position > totalPages) {
|
||||||
|
showAlert(
|
||||||
|
'Invalid Input',
|
||||||
|
`Please enter a number between 0 and ${totalPages}.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(pageCount) || pageCount < 1) {
|
||||||
|
showAlert(
|
||||||
|
'Invalid Input',
|
||||||
|
'Please enter a valid number of pages (1 or more).'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader(`Adding ${pageCount} blank page${pageCount > 1 ? 's' : ''}...`);
|
||||||
|
try {
|
||||||
|
const newPdf = await PDFLibDocument.create();
|
||||||
|
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
||||||
|
const allIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
|
|
||||||
|
const indicesBefore = allIndices.slice(0, position);
|
||||||
|
const indicesAfter = allIndices.slice(position);
|
||||||
|
|
||||||
|
if (indicesBefore.length > 0) {
|
||||||
|
const copied = await newPdf.copyPages(state.pdfDoc, indicesBefore);
|
||||||
|
copied.forEach((p: any) => newPdf.addPage(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = parseInt(pageNumberInput);
|
// Add the specified number of blank pages
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
for (let i = 0; i < pageCount; i++) {
|
||||||
if (isNaN(position) || position < 0 || position > totalPages) {
|
newPdf.addPage([width, height]);
|
||||||
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Adding page...');
|
if (indicesAfter.length > 0) {
|
||||||
try {
|
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
||||||
const newPdf = await PDFLibDocument.create();
|
copied.forEach((p: any) => newPdf.addPage(p));
|
||||||
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
|
||||||
const allIndices = Array.from({ length: totalPages }, (_, i) => i);
|
|
||||||
|
|
||||||
const indicesBefore = allIndices.slice(0, position);
|
|
||||||
const indicesAfter = allIndices.slice(position);
|
|
||||||
|
|
||||||
if (indicesBefore.length > 0) {
|
|
||||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesBefore);
|
|
||||||
copied.forEach((p: any) => newPdf.addPage(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
newPdf.addPage([width, height]);
|
|
||||||
|
|
||||||
if (indicesAfter.length > 0) {
|
|
||||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
|
||||||
copied.forEach((p: any) => newPdf.addPage(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'page-added.pdf');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Could not add a blank page.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newPdfBytes = await newPdf.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
`blank-page${pageCount > 1 ? 's' : ''}-added.pdf`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', `Could not add blank page${pageCount > 1 ? 's' : ''}.`);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,98 +4,154 @@ 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) {
|
||||||
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
|
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addHeaderFooter() {
|
export async function addHeaderFooter() {
|
||||||
showLoader('Adding header & footer...');
|
showLoader('Adding header & footer...');
|
||||||
try {
|
try {
|
||||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
const allPages = state.pdfDoc.getPages();
|
const allPages = state.pdfDoc.getPages();
|
||||||
const totalPages = allPages.length;
|
const totalPages = allPages.length;
|
||||||
const margin = 40;
|
const margin = 40;
|
||||||
|
|
||||||
// --- 1. Get new formatting options from the UI ---
|
// --- 1. Get new formatting options from the UI ---
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
|
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const colorHex = document.getElementById('font-color').value;
|
const colorHex = document.getElementById('font-color').value;
|
||||||
const fontColor = hexToRgb(colorHex);
|
const fontColor = hexToRgb(colorHex);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const pageRangeInput = document.getElementById('page-range').value;
|
const pageRangeInput = document.getElementById('page-range').value;
|
||||||
|
|
||||||
// --- 2. Get text values ---
|
// --- 2. Get text values ---
|
||||||
const texts = {
|
const texts = {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
headerLeft: document.getElementById('header-left').value,
|
headerLeft: document.getElementById('header-left').value,
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
headerCenter: document.getElementById('header-center').value,
|
headerCenter: document.getElementById('header-center').value,
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
headerRight: document.getElementById('header-right').value,
|
headerRight: document.getElementById('header-right').value,
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
footerLeft: document.getElementById('footer-left').value,
|
footerLeft: document.getElementById('footer-left').value,
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
footerCenter: document.getElementById('footer-center').value,
|
footerCenter: document.getElementById('footer-center').value,
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
footerRight: document.getElementById('footer-right').value,
|
footerRight: document.getElementById('footer-right').value,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 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 ---
|
|
||||||
const drawOptions = {
|
|
||||||
font: helveticaFont,
|
|
||||||
size: fontSize,
|
|
||||||
color: rgb(fontColor.r, fontColor.g, fontColor.b)
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- 5. Loop over only the selected pages ---
|
|
||||||
for (const pageIndex of indicesToProcess) {
|
|
||||||
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
|
||||||
const page = allPages[pageIndex];
|
|
||||||
const { width, height } = page.getSize();
|
|
||||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
|
||||||
const pageNumber = pageIndex + 1; // For dynamic text
|
|
||||||
|
|
||||||
// Helper to replace placeholders like {page} and {total}
|
|
||||||
const processText = (text: any) => text
|
|
||||||
.replace(/{page}/g, pageNumber)
|
|
||||||
.replace(/{total}/g, totalPages);
|
|
||||||
|
|
||||||
// Get processed text for the current page
|
|
||||||
const processedTexts = {
|
|
||||||
headerLeft: processText(texts.headerLeft),
|
|
||||||
headerCenter: processText(texts.headerCenter),
|
|
||||||
headerRight: processText(texts.headerRight),
|
|
||||||
footerLeft: processText(texts.footerLeft),
|
|
||||||
footerCenter: processText(texts.footerCenter),
|
|
||||||
footerRight: processText(texts.footerRight),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (processedTexts.headerLeft) page.drawText(processedTexts.headerLeft, { ...drawOptions, x: margin, y: height - margin });
|
|
||||||
if (processedTexts.headerCenter) page.drawText(processedTexts.headerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.headerCenter, fontSize) / 2, y: height - margin });
|
|
||||||
if (processedTexts.headerRight) page.drawText(processedTexts.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.headerRight, fontSize), y: height - margin });
|
|
||||||
if (processedTexts.footerLeft) page.drawText(processedTexts.footerLeft, { ...drawOptions, x: margin, y: margin });
|
|
||||||
if (processedTexts.footerCenter) page.drawText(processedTexts.footerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.footerCenter, fontSize) / 2, y: margin });
|
|
||||||
if (processedTexts.footerRight) page.drawText(processedTexts.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.footerRight, fontSize), y: margin });
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'header-footer-added.pdf');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', e.message || 'Could not add header or footer.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 4. Define drawing options with new values ---
|
||||||
|
const drawOptions = {
|
||||||
|
font: helveticaFont,
|
||||||
|
size: fontSize,
|
||||||
|
color: rgb(fontColor.r, fontColor.g, fontColor.b),
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- 5. Loop over only the selected pages ---
|
||||||
|
for (const pageIndex of indicesToProcess) {
|
||||||
|
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
|
||||||
|
const page = allPages[pageIndex];
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||||
|
const pageNumber = pageIndex + 1; // For dynamic text
|
||||||
|
|
||||||
|
// Helper to replace placeholders like {page} and {total}
|
||||||
|
const processText = (text: any) =>
|
||||||
|
text.replace(/{page}/g, pageNumber).replace(/{total}/g, totalPages);
|
||||||
|
|
||||||
|
// Get processed text for the current page
|
||||||
|
const processedTexts = {
|
||||||
|
headerLeft: processText(texts.headerLeft),
|
||||||
|
headerCenter: processText(texts.headerCenter),
|
||||||
|
headerRight: processText(texts.headerRight),
|
||||||
|
footerLeft: processText(texts.footerLeft),
|
||||||
|
footerCenter: processText(texts.footerCenter),
|
||||||
|
footerRight: processText(texts.footerRight),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (processedTexts.headerLeft)
|
||||||
|
page.drawText(processedTexts.headerLeft, {
|
||||||
|
...drawOptions,
|
||||||
|
x: margin,
|
||||||
|
y: height - margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.headerCenter)
|
||||||
|
page.drawText(processedTexts.headerCenter, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width / 2 -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.headerCenter,
|
||||||
|
fontSize
|
||||||
|
) /
|
||||||
|
2,
|
||||||
|
y: height - margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.headerRight)
|
||||||
|
page.drawText(processedTexts.headerRight, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width -
|
||||||
|
margin -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.headerRight,
|
||||||
|
fontSize
|
||||||
|
),
|
||||||
|
y: height - margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.footerLeft)
|
||||||
|
page.drawText(processedTexts.footerLeft, {
|
||||||
|
...drawOptions,
|
||||||
|
x: margin,
|
||||||
|
y: margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.footerCenter)
|
||||||
|
page.drawText(processedTexts.footerCenter, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width / 2 -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.footerCenter,
|
||||||
|
fontSize
|
||||||
|
) /
|
||||||
|
2,
|
||||||
|
y: margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.footerRight)
|
||||||
|
page.drawText(processedTexts.footerRight, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width -
|
||||||
|
margin -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.footerRight,
|
||||||
|
fontSize
|
||||||
|
),
|
||||||
|
y: margin,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'header-footer-added.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', e.message || 'Could not add header or footer.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,97 +5,133 @@ import { state } from '../state.js';
|
|||||||
import { rgb, StandardFonts } from 'pdf-lib';
|
import { rgb, StandardFonts } from 'pdf-lib';
|
||||||
|
|
||||||
export async function addPageNumbers() {
|
export async function addPageNumbers() {
|
||||||
showLoader('Adding page numbers...');
|
showLoader('Adding page numbers...');
|
||||||
try {
|
try {
|
||||||
// @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 position = document.getElementById('position').value;
|
const position = document.getElementById('position').value;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const format = document.getElementById('number-format').value;
|
const format = document.getElementById('number-format').value;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const colorHex = document.getElementById('text-color').value;
|
const colorHex = document.getElementById('text-color').value;
|
||||||
const textColor = hexToRgb(colorHex);
|
const textColor = hexToRgb(colorHex);
|
||||||
|
|
||||||
const pages = state.pdfDoc.getPages();
|
const pages = state.pdfDoc.getPages();
|
||||||
const totalPages = pages.length;
|
const totalPages = pages.length;
|
||||||
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
|
||||||
for (let i = 0; i < totalPages; i++) {
|
for (let i = 0; i < totalPages; i++) {
|
||||||
const page = pages[i];
|
const page = pages[i];
|
||||||
|
|
||||||
const mediaBox = page.getMediaBox();
|
const mediaBox = page.getMediaBox();
|
||||||
const cropBox = page.getCropBox();
|
const cropBox = page.getCropBox();
|
||||||
const bounds = cropBox || mediaBox;
|
const bounds = cropBox || mediaBox;
|
||||||
const width = bounds.width;
|
const width = bounds.width;
|
||||||
const height = bounds.height;
|
const height = bounds.height;
|
||||||
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;
|
||||||
|
|
||||||
const minMargin = 8;
|
const minMargin = 8;
|
||||||
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(
|
||||||
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
horizontalMargin,
|
||||||
|
textWidth / 2 + 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 =
|
||||||
y = safeVerticalMargin + yOffset;
|
Math.max(
|
||||||
break;
|
safeHorizontalMargin,
|
||||||
case 'bottom-left':
|
Math.min(
|
||||||
x = safeHorizontalMargin + xOffset;
|
width - safeHorizontalMargin - textWidth,
|
||||||
y = safeVerticalMargin + yOffset;
|
(width - textWidth) / 2
|
||||||
break;
|
)
|
||||||
case 'bottom-right':
|
) + xOffset;
|
||||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
y = safeVerticalMargin + yOffset;
|
||||||
y = safeVerticalMargin + yOffset;
|
break;
|
||||||
break;
|
case 'bottom-left':
|
||||||
case 'top-center':
|
x = safeHorizontalMargin + xOffset;
|
||||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
y = safeVerticalMargin + yOffset;
|
||||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
break;
|
||||||
break;
|
case 'bottom-right':
|
||||||
case 'top-left':
|
x =
|
||||||
x = safeHorizontalMargin + xOffset;
|
Math.max(
|
||||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
safeHorizontalMargin,
|
||||||
break;
|
width - safeHorizontalMargin - textWidth
|
||||||
case 'top-right':
|
) + xOffset;
|
||||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
y = safeVerticalMargin + yOffset;
|
||||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
break;
|
||||||
break;
|
case 'top-center':
|
||||||
}
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
Math.min(
|
||||||
|
width - safeHorizontalMargin - textWidth,
|
||||||
|
(width - textWidth) / 2
|
||||||
|
)
|
||||||
|
) + xOffset;
|
||||||
|
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||||
|
break;
|
||||||
|
case 'top-left':
|
||||||
|
x = safeHorizontalMargin + xOffset;
|
||||||
|
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||||
|
break;
|
||||||
|
case 'top-right':
|
||||||
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
width - safeHorizontalMargin - textWidth
|
||||||
|
) + xOffset;
|
||||||
|
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Final safety check to ensure coordinates are within visible page bounds
|
// Final safety check to ensure coordinates are within visible page bounds
|
||||||
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
|
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
|
||||||
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,
|
||||||
font: helveticaFont,
|
y,
|
||||||
size: fontSize,
|
font: helveticaFont,
|
||||||
color: rgb(textColor.r, textColor.g, textColor.b)
|
size: fontSize,
|
||||||
});
|
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||||
}
|
});
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'paginated.pdf');
|
|
||||||
showAlert('Success', 'Page numbers added successfully!');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Could not add page numbers.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'paginated.pdf'
|
||||||
|
);
|
||||||
|
showAlert('Success', 'Page numbers added successfully!');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not add page numbers.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+167
-112
@@ -1,132 +1,187 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, hexToRgb } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
hexToRgb,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } 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(
|
||||||
const textOptions = document.getElementById('text-watermark-options');
|
'input[name="watermark-type"]'
|
||||||
const imageOptions = document.getElementById('image-watermark-options');
|
);
|
||||||
|
const textOptions = document.getElementById('text-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') {
|
||||||
textOptions.classList.remove('hidden');
|
textOptions.classList.remove('hidden');
|
||||||
imageOptions.classList.add('hidden');
|
imageOptions.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
textOptions.classList.add('hidden');
|
textOptions.classList.add('hidden');
|
||||||
imageOptions.classList.remove('hidden');
|
imageOptions.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const opacitySliderText = document.getElementById('opacity-text');
|
const opacitySliderText = document.getElementById('opacity-text');
|
||||||
const opacityValueText = document.getElementById('opacity-value-text');
|
const opacityValueText = document.getElementById('opacity-value-text');
|
||||||
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)
|
||||||
|
);
|
||||||
|
|
||||||
const opacitySliderImage = document.getElementById('opacity-image');
|
angleSliderText.addEventListener(
|
||||||
const opacityValueImage = document.getElementById('opacity-value-image');
|
'input',
|
||||||
const angleSliderImage = document.getElementById('angle-image');
|
() =>
|
||||||
const angleValueImage = document.getElementById('angle-value-image');
|
(angleValueText.textContent = (angleSliderText 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
|
const opacitySliderImage = document.getElementById('opacity-image');
|
||||||
opacitySliderImage.addEventListener('input', () => opacityValueImage.textContent = opacitySliderImage.value);
|
const opacityValueImage = document.getElementById('opacity-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
|
const angleSliderImage = document.getElementById('angle-image');
|
||||||
angleSliderImage.addEventListener('input', () => angleValueImage.textContent = angleSliderImage.value);
|
const angleValueImage = document.getElementById('angle-value-image');
|
||||||
|
|
||||||
|
opacitySliderImage.addEventListener(
|
||||||
|
'input',
|
||||||
|
() =>
|
||||||
|
(opacityValueImage.textContent = (
|
||||||
|
opacitySliderImage as HTMLInputElement
|
||||||
|
).value)
|
||||||
|
);
|
||||||
|
|
||||||
|
angleSliderImage.addEventListener(
|
||||||
|
'input',
|
||||||
|
() =>
|
||||||
|
(angleValueImage.textContent = (
|
||||||
|
angleSliderImage as HTMLInputElement
|
||||||
|
).value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addWatermark() {
|
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...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pages = state.pdfDoc.getPages();
|
const pages = state.pdfDoc.getPages();
|
||||||
let watermarkAsset = null;
|
let watermarkAsset = null;
|
||||||
|
|
||||||
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') {
|
||||||
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
|
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
|
||||||
} 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.'
|
||||||
}
|
);
|
||||||
|
}
|
||||||
for (const page of pages) {
|
|
||||||
const { width, height } = page.getSize();
|
|
||||||
|
|
||||||
if (watermarkType === 'text') {
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const text = document.getElementById('watermark-text').value;
|
|
||||||
if (!text.trim()) throw new Error('Please enter text for the watermark.');
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const fontSize = parseInt(document.getElementById('font-size').value) || 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 = parseInt(document.getElementById('angle-text').value) || 0;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const opacity = parseFloat(document.getElementById('opacity-text').value) || 0.3;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const colorHex = document.getElementById('text-color').value;
|
|
||||||
const textColor = hexToRgb(colorHex);
|
|
||||||
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
|
||||||
|
|
||||||
page.drawText(text, {
|
|
||||||
x: (width - textWidth) / 2,
|
|
||||||
y: height / 2,
|
|
||||||
font: watermarkAsset,
|
|
||||||
size: fontSize,
|
|
||||||
color: rgb(textColor.r, textColor.g, textColor.b),
|
|
||||||
opacity: opacity,
|
|
||||||
rotate: degrees(angle),
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// @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 = parseInt(document.getElementById('angle-image').value) || 0;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const opacity = parseFloat(document.getElementById('opacity-image').value) || 0.3;
|
|
||||||
|
|
||||||
const scale = 0.5;
|
|
||||||
const imgWidth = watermarkAsset.width * scale;
|
|
||||||
const imgHeight = watermarkAsset.height * scale;
|
|
||||||
|
|
||||||
page.drawImage(watermarkAsset, {
|
|
||||||
x: (width - imgWidth) / 2,
|
|
||||||
y: (height - imgHeight) / 2,
|
|
||||||
width: imgWidth,
|
|
||||||
height: imgHeight,
|
|
||||||
opacity: opacity,
|
|
||||||
rotate: degrees(angle),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'watermarked.pdf');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', e.message || 'Could not add the watermark. Please check your inputs.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const page of pages) {
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
|
||||||
|
if (watermarkType === 'text') {
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const text = document.getElementById('watermark-text').value;
|
||||||
|
if (!text.trim())
|
||||||
|
throw new Error('Please enter text for the watermark.');
|
||||||
|
|
||||||
|
const fontSize =
|
||||||
|
parseInt(
|
||||||
|
(document.getElementById('font-size') as HTMLInputElement).value
|
||||||
|
) || 72;
|
||||||
|
const angle =
|
||||||
|
parseInt(
|
||||||
|
(document.getElementById('angle-text') as HTMLInputElement).value
|
||||||
|
) || 0;
|
||||||
|
const opacity =
|
||||||
|
parseFloat(
|
||||||
|
(document.getElementById('opacity-text') as HTMLInputElement).value
|
||||||
|
) || 0.3;
|
||||||
|
const colorHex = (
|
||||||
|
document.getElementById('text-color') as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
const textColor = hexToRgb(colorHex);
|
||||||
|
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
||||||
|
|
||||||
|
page.drawText(text, {
|
||||||
|
x: (width - textWidth) / 2,
|
||||||
|
y: height / 2,
|
||||||
|
font: watermarkAsset,
|
||||||
|
size: fontSize,
|
||||||
|
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||||
|
opacity: opacity,
|
||||||
|
rotate: degrees(angle),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const angle =
|
||||||
|
parseInt(
|
||||||
|
(document.getElementById('angle-image') as HTMLInputElement).value
|
||||||
|
) || 0;
|
||||||
|
const opacity =
|
||||||
|
parseFloat(
|
||||||
|
(document.getElementById('opacity-image') as HTMLInputElement).value
|
||||||
|
) || 0.3;
|
||||||
|
|
||||||
|
const scale = 0.5;
|
||||||
|
const imgWidth = watermarkAsset.width * scale;
|
||||||
|
const imgHeight = watermarkAsset.height * scale;
|
||||||
|
|
||||||
|
page.drawImage(watermarkAsset, {
|
||||||
|
x: (width - imgWidth) / 2,
|
||||||
|
y: (height - imgHeight) / 2,
|
||||||
|
width: imgWidth,
|
||||||
|
height: imgHeight,
|
||||||
|
opacity: opacity,
|
||||||
|
rotate: degrees(angle),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'watermarked.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
e.message || 'Could not add the watermark. Please check your inputs.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
-42
@@ -5,51 +5,64 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
async function convertImageToPngBytes(file: any) {
|
async function convertImageToPngBytes(file: any) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = img.width;
|
canvas.width = img.width;
|
||||||
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) =>
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
canvas.toBlob(res, 'image/png')
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
);
|
||||||
resolve(pngBytes);
|
// @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();
|
||||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
resolve(pngBytes);
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
};
|
||||||
img.src = e.target.result;
|
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||||
};
|
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
||||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
img.src = e.target.result;
|
||||||
reader.readAsDataURL(file);
|
};
|
||||||
});
|
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bmpToPdf() {
|
export async function bmpToPdf() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one BMP file.');
|
showAlert('No Files', 'Please select at least one BMP file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Converting BMP to PDF...');
|
showLoader('Converting BMP to PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfDoc = await PDFLibDocument.create();
|
const pdfDoc = await PDFLibDocument.create();
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
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,
|
||||||
const pdfBytes = await pdfDoc.save();
|
y: 0,
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_bmps.pdf');
|
width: pngImage.width,
|
||||||
} catch (e) {
|
height: pngImage.height,
|
||||||
console.error(e);
|
});
|
||||||
showAlert('Error', 'Failed to convert BMP to PDF. One of the files may be invalid.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_bmps.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert BMP to PDF. One of the files may be invalid.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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';
|
||||||
@@ -6,48 +5,51 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||||
|
|
||||||
export async function changeBackgroundColor() {
|
export async function changeBackgroundColor() {
|
||||||
if (!state.pdfDoc) {
|
if (!state.pdfDoc) {
|
||||||
showAlert('Error', 'PDF not loaded.');
|
showAlert('Error', 'PDF not loaded.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const colorHex = document.getElementById('background-color').value;
|
||||||
|
const color = hexToRgb(colorHex);
|
||||||
|
|
||||||
|
showLoader('Changing background color...');
|
||||||
|
try {
|
||||||
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
|
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
|
||||||
|
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||||
|
const { width, height } = originalPage.getSize();
|
||||||
|
|
||||||
|
const newPage = newPdfDoc.addPage([width, height]);
|
||||||
|
|
||||||
|
newPage.drawRectangle({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
color: rgb(color.r, color.g, color.b),
|
||||||
|
});
|
||||||
|
|
||||||
|
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
||||||
|
newPage.drawPage(embeddedPage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
const colorHex = document.getElementById('background-color').value;
|
downloadFile(
|
||||||
const color = hexToRgb(colorHex);
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'background-changed.pdf'
|
||||||
showLoader('Changing background color...');
|
);
|
||||||
try {
|
} catch (e) {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not change the background color.');
|
||||||
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
|
} finally {
|
||||||
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
hideLoader();
|
||||||
const { width, height } = originalPage.getSize();
|
}
|
||||||
|
|
||||||
const newPage = newPdfDoc.addPage([width, height]);
|
|
||||||
|
|
||||||
newPage.drawRectangle({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
color: rgb(color.r, color.g, color.b),
|
|
||||||
});
|
|
||||||
|
|
||||||
const embeddedPage = await newPdfDoc.embedPage(originalPage);
|
|
||||||
newPage.drawPage(embeddedPage, {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Could not change the background color.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+131
-100
@@ -1,115 +1,146 @@
|
|||||||
|
|
||||||
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 PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||||
import blobStream from 'blob-stream';
|
import blobStream from 'blob-stream';
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
export async function changePermissions() {
|
export async function changePermissions() {
|
||||||
const currentPassword = (document.getElementById('current-password') as HTMLInputElement).value;
|
const currentPassword = (
|
||||||
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement).value;
|
document.getElementById('current-password') as HTMLInputElement
|
||||||
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement).value;
|
).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.
|
// An owner password is required to enforce any permissions.
|
||||||
if (!newOwnerPassword && (newUserPassword || document.querySelectorAll('input[type="checkbox"]:not(:checked)').length > 0)) {
|
if (
|
||||||
showAlert('Input Required', 'You must set a "New Owner Password" to enforce specific permissions or to set a user password.');
|
!newOwnerPassword &&
|
||||||
return;
|
(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...');
|
showLoader('Preparing to process...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = state.files[0];
|
||||||
|
const pdfData = await readFileAsArrayBuffer(file);
|
||||||
|
|
||||||
|
let pdf;
|
||||||
try {
|
try {
|
||||||
const file = state.files[0];
|
pdf = await pdfjsLib.getDocument({
|
||||||
const pdfData = await readFileAsArrayBuffer(file);
|
data: pdfData as ArrayBuffer,
|
||||||
|
password: currentPassword,
|
||||||
let pdf;
|
}).promise;
|
||||||
try {
|
|
||||||
pdf = await pdfjsLib.getDocument({
|
|
||||||
data: pdfData as ArrayBuffer,
|
|
||||||
password: currentPassword
|
|
||||||
}).promise;
|
|
||||||
} catch (e) {
|
|
||||||
// This catch is specific to password errors in pdf.js
|
|
||||||
if (e.name === 'PasswordException') {
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Incorrect Password', 'The current password you entered is incorrect.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numPages = pdf.numPages;
|
|
||||||
const pageImages = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= numPages; i++) {
|
|
||||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
|
||||||
const page = await pdf.getPage(i);
|
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
|
||||||
pageImages.push({
|
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
|
||||||
width: viewport.width,
|
|
||||||
height: viewport.height
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent = 'Applying new permissions...';
|
|
||||||
|
|
||||||
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement).checked;
|
|
||||||
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement).checked;
|
|
||||||
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement).checked;
|
|
||||||
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement).checked;
|
|
||||||
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement).checked;
|
|
||||||
const allowContentAccessibility = (document.getElementById('allow-content-accessibility') as HTMLInputElement).checked;
|
|
||||||
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement).checked;
|
|
||||||
|
|
||||||
|
|
||||||
const doc = new PDFDocument({
|
|
||||||
size: [pageImages[0].width, pageImages[0].height],
|
|
||||||
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
|
|
||||||
|
|
||||||
// Apply the new, separate user and owner passwords
|
|
||||||
userPassword: newUserPassword,
|
|
||||||
ownerPassword: newOwnerPassword,
|
|
||||||
|
|
||||||
// Apply all seven permissions from the checkboxes
|
|
||||||
permissions: {
|
|
||||||
printing: allowPrinting ? 'highResolution' : false,
|
|
||||||
modifying: allowModifying,
|
|
||||||
copying: allowCopying,
|
|
||||||
annotating: allowAnnotating,
|
|
||||||
fillingForms: allowFillingForms,
|
|
||||||
contentAccessibility: allowContentAccessibility,
|
|
||||||
documentAssembly: allowDocumentAssembly
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const stream = doc.pipe(blobStream());
|
|
||||||
|
|
||||||
for (let i = 0; i < pageImages.length; i++) {
|
|
||||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
|
||||||
doc.image(pageImages[i].data, 0, 0, {
|
|
||||||
width: pageImages[i].width,
|
|
||||||
height: pageImages[i].height
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.end();
|
|
||||||
stream.on('finish', function () {
|
|
||||||
const blob = stream.toBlob('application/pdf');
|
|
||||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Success', 'Permissions changed successfully!');
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
// This catch is specific to password errors in pdf.js
|
||||||
|
if (e.name === 'PasswordException') {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An unexpected error occurred: ${e.message}`);
|
showAlert(
|
||||||
|
'Incorrect Password',
|
||||||
|
'The current password you entered is incorrect.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const numPages = pdf.numPages;
|
||||||
|
const pageImages = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= numPages; i++) {
|
||||||
|
document.getElementById('loader-text').textContent =
|
||||||
|
`Processing page ${i} of ${numPages}...`;
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const viewport = page.getViewport({ scale: 2.0 });
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
|
pageImages.push({
|
||||||
|
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('loader-text').textContent =
|
||||||
|
'Applying new permissions...';
|
||||||
|
|
||||||
|
const allowPrinting = (
|
||||||
|
document.getElementById('allow-printing') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const allowCopying = (
|
||||||
|
document.getElementById('allow-copying') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const allowModifying = (
|
||||||
|
document.getElementById('allow-modifying') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const allowAnnotating = (
|
||||||
|
document.getElementById('allow-annotating') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const allowFillingForms = (
|
||||||
|
document.getElementById('allow-filling-forms') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const allowContentAccessibility = (
|
||||||
|
document.getElementById('allow-content-accessibility') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
const allowDocumentAssembly = (
|
||||||
|
document.getElementById('allow-document-assembly') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
|
||||||
|
const doc = new PDFDocument({
|
||||||
|
size: [pageImages[0].width, pageImages[0].height],
|
||||||
|
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
|
||||||
|
|
||||||
|
// Apply the new, separate user and owner passwords
|
||||||
|
userPassword: newUserPassword,
|
||||||
|
ownerPassword: newOwnerPassword,
|
||||||
|
|
||||||
|
// Apply all seven permissions from the checkboxes
|
||||||
|
permissions: {
|
||||||
|
printing: allowPrinting ? 'highResolution' : false,
|
||||||
|
modifying: allowModifying,
|
||||||
|
copying: allowCopying,
|
||||||
|
annotating: allowAnnotating,
|
||||||
|
fillingForms: allowFillingForms,
|
||||||
|
contentAccessibility: allowContentAccessibility,
|
||||||
|
documentAssembly: allowDocumentAssembly,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const stream = doc.pipe(blobStream());
|
||||||
|
|
||||||
|
for (let i = 0; i < pageImages.length; i++) {
|
||||||
|
if (i > 0)
|
||||||
|
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||||
|
doc.image(pageImages[i].data, 0, 0, {
|
||||||
|
width: pageImages[i].width,
|
||||||
|
height: pageImages[i].height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.end();
|
||||||
|
stream.on('finish', function () {
|
||||||
|
const blob = stream.toBlob('application/pdf');
|
||||||
|
downloadFile(blob, `permissions-changed-${file.name}`);
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Success', 'Permissions changed successfully!');
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', `An unexpected error occurred: ${e.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+155
-122
@@ -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';
|
||||||
@@ -9,136 +12,166 @@ let isRenderingPreview = false;
|
|||||||
let renderTimeout: any;
|
let renderTimeout: any;
|
||||||
|
|
||||||
async function updateTextColorPreview() {
|
async function updateTextColorPreview() {
|
||||||
if (isRenderingPreview) return;
|
if (isRenderingPreview) return;
|
||||||
isRenderingPreview = true;
|
isRenderingPreview = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const textColorCanvas = document.getElementById('text-color-canvas');
|
const textColorCanvas = document.getElementById('text-color-canvas');
|
||||||
if (!textColorCanvas) return;
|
if (!textColorCanvas) return;
|
||||||
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
|
||||||
const page = await pdf.getPage(1); // Preview first page
|
|
||||||
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
|
|
||||||
const context = textColorCanvas.getContext('2d');
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
textColorCanvas.width = viewport.width;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
|
||||||
textColorCanvas.height = viewport.height;
|
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const imageData = context.getImageData(0, 0, textColorCanvas.width, textColorCanvas.height);
|
|
||||||
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 = document.getElementById('text-color-input').value;
|
|
||||||
const { r, g, b } = hexToRgb(colorHex);
|
|
||||||
const darknessThreshold = 120;
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
|
||||||
if (data[i] < darknessThreshold && data[i + 1] < darknessThreshold && data[i + 2] < darknessThreshold) {
|
|
||||||
data[i] = r * 255;
|
|
||||||
data[i + 1] = g * 255;
|
|
||||||
data[i + 2] = b * 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.putImageData(imageData, 0, 0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating preview:', error);
|
|
||||||
} finally {
|
|
||||||
isRenderingPreview = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setupTextColorTool() {
|
|
||||||
const originalCanvas = document.getElementById('original-canvas');
|
|
||||||
const colorInput = document.getElementById('text-color-input');
|
|
||||||
|
|
||||||
if (!originalCanvas || !colorInput) return;
|
|
||||||
|
|
||||||
// Debounce the preview update for performance
|
|
||||||
colorInput.addEventListener('input', () => {
|
|
||||||
clearTimeout(renderTimeout);
|
|
||||||
renderTimeout = setTimeout(updateTextColorPreview, 250);
|
|
||||||
});
|
|
||||||
|
|
||||||
// @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(
|
||||||
const page = await pdf.getPage(1);
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
|
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
|
||||||
|
const context = textColorCanvas.getContext('2d');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
originalCanvas.width = viewport.width;
|
textColorCanvas.width = viewport.width;
|
||||||
// @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;
|
textColorCanvas.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({ canvasContext: context, viewport }).promise;
|
||||||
await page.render({ canvasContext: originalCanvas.getContext('2d'), viewport }).promise;
|
const imageData = context.getImageData(
|
||||||
await updateTextColorPreview();
|
0,
|
||||||
}
|
0,
|
||||||
|
(textColorCanvas as HTMLCanvasElement).width,
|
||||||
export async function changeTextColor() {
|
(textColorCanvas as HTMLCanvasElement).height
|
||||||
if (!state.pdfDoc) {
|
);
|
||||||
showAlert('Error', 'PDF not loaded.');
|
const data = imageData.data;
|
||||||
return;
|
const colorHex = (
|
||||||
}
|
document.getElementById('text-color-input') 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
|
|
||||||
const colorHex = document.getElementById('text-color-input').value;
|
|
||||||
const { r, g, b } = hexToRgb(colorHex);
|
const { r, g, b } = hexToRgb(colorHex);
|
||||||
const darknessThreshold = 120;
|
const darknessThreshold = 120;
|
||||||
|
|
||||||
showLoader('Changing text color...');
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
try {
|
if (
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
data[i] < darknessThreshold &&
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
data[i + 1] < darknessThreshold &&
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
data[i + 2] < darknessThreshold
|
||||||
|
) {
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
data[i] = r * 255;
|
||||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
data[i + 1] = g * 255;
|
||||||
const page = await pdf.getPage(i);
|
data[i + 2] = b * 255;
|
||||||
const viewport = page.getViewport({ scale: 2.0 }); // High resolution for quality
|
}
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
|
||||||
|
|
||||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
|
||||||
const data = imageData.data;
|
|
||||||
|
|
||||||
for (let j = 0; j < data.length; j += 4) {
|
|
||||||
if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) {
|
|
||||||
data[j] = r * 255;
|
|
||||||
data[j + 1] = g * 255;
|
|
||||||
data[j + 2] = b * 255;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
context.putImageData(imageData, 0, 0);
|
|
||||||
|
|
||||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
|
||||||
reader.readAsArrayBuffer(blob);
|
|
||||||
}, 'image/png'));
|
|
||||||
|
|
||||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
|
||||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
|
||||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'text-color-changed.pdf');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Could not change text color.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating preview:', error);
|
||||||
|
} finally {
|
||||||
|
isRenderingPreview = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupTextColorTool() {
|
||||||
|
const originalCanvas = document.getElementById('original-canvas');
|
||||||
|
const colorInput = document.getElementById('text-color-input');
|
||||||
|
|
||||||
|
if (!originalCanvas || !colorInput) return;
|
||||||
|
|
||||||
|
// Debounce the preview update for performance
|
||||||
|
colorInput.addEventListener('input', () => {
|
||||||
|
clearTimeout(renderTimeout);
|
||||||
|
renderTimeout = setTimeout(updateTextColorPreview, 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
|
const page = await pdf.getPage(1);
|
||||||
|
const viewport = page.getViewport({ scale: 0.8 });
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
originalCanvas.width = viewport.width;
|
||||||
|
// @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;
|
||||||
|
|
||||||
|
await page.render({
|
||||||
|
canvasContext: (originalCanvas as HTMLCanvasElement).getContext('2d'),
|
||||||
|
viewport,
|
||||||
|
}).promise;
|
||||||
|
await updateTextColorPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changeTextColor() {
|
||||||
|
if (!state.pdfDoc) {
|
||||||
|
showAlert('Error', 'PDF not loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const colorHex = document.getElementById('text-color-input').value;
|
||||||
|
const { r, g, b } = hexToRgb(colorHex);
|
||||||
|
const darknessThreshold = 120;
|
||||||
|
|
||||||
|
showLoader('Changing text color...');
|
||||||
|
try {
|
||||||
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const viewport = page.getViewport({ scale: 2.0 }); // High resolution for quality
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
|
|
||||||
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
for (let j = 0; j < data.length; j += 4) {
|
||||||
|
if (
|
||||||
|
data[j] < darknessThreshold &&
|
||||||
|
data[j + 1] < darknessThreshold &&
|
||||||
|
data[j + 2] < darknessThreshold
|
||||||
|
) {
|
||||||
|
data[j] = r * 255;
|
||||||
|
data[j + 1] = g * 255;
|
||||||
|
data[j + 2] = b * 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
const pngImageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
|
reader.readAsArrayBuffer(blob);
|
||||||
|
}, 'image/png')
|
||||||
|
);
|
||||||
|
|
||||||
|
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||||
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
|
newPage.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'text-color-changed.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not change text color.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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';
|
||||||
@@ -6,72 +5,74 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||||
|
|
||||||
export async function combineToSinglePage() {
|
export async function combineToSinglePage() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
|
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const backgroundColorHex = document.getElementById('background-color').value;
|
const backgroundColorHex = document.getElementById('background-color').value;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||||
const addSeparator = document.getElementById('add-separator').checked;
|
const addSeparator = document.getElementById('add-separator').checked;
|
||||||
const backgroundColor = hexToRgb(backgroundColorHex);
|
const backgroundColor = hexToRgb(backgroundColorHex);
|
||||||
|
|
||||||
showLoader('Combining pages...');
|
showLoader('Combining pages...');
|
||||||
try {
|
try {
|
||||||
const sourceDoc = state.pdfDoc;
|
const sourceDoc = state.pdfDoc;
|
||||||
const newDoc = await PDFLibDocument.create();
|
const newDoc = await PDFLibDocument.create();
|
||||||
const sourcePages = sourceDoc.getPages();
|
const sourcePages = sourceDoc.getPages();
|
||||||
|
|
||||||
let maxWidth = 0;
|
let maxWidth = 0;
|
||||||
let totalHeight = 0;
|
let totalHeight = 0;
|
||||||
sourcePages.forEach((page: any) => {
|
sourcePages.forEach((page: any) => {
|
||||||
const { width, height } = page.getSize();
|
const { width, height } = page.getSize();
|
||||||
if (width > maxWidth) maxWidth = width;
|
if (width > maxWidth) maxWidth = width;
|
||||||
totalHeight += height;
|
totalHeight += height;
|
||||||
});
|
});
|
||||||
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
|
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
|
||||||
|
|
||||||
const newPage = newDoc.addPage([maxWidth, totalHeight]);
|
const newPage = newDoc.addPage([maxWidth, totalHeight]);
|
||||||
|
|
||||||
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
|
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
|
||||||
newPage.drawRectangle({
|
newPage.drawRectangle({
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: maxWidth,
|
width: maxWidth,
|
||||||
height: totalHeight,
|
height: totalHeight,
|
||||||
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
let currentY = totalHeight;
|
|
||||||
for (let i = 0; i < sourcePages.length; i++) {
|
|
||||||
const sourcePage = sourcePages[i];
|
|
||||||
const { width, height } = sourcePage.getSize();
|
|
||||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
|
||||||
|
|
||||||
currentY -= height;
|
|
||||||
const x = (maxWidth - width) / 2;
|
|
||||||
|
|
||||||
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
|
||||||
|
|
||||||
if (addSeparator && i < sourcePages.length - 1) {
|
|
||||||
const lineY = currentY - (spacing / 2);
|
|
||||||
newPage.drawLine({
|
|
||||||
start: { x: 0, y: lineY },
|
|
||||||
end: { x: maxWidth, y: lineY },
|
|
||||||
thickness: 0.5,
|
|
||||||
color: rgb(0.8, 0.8, 0.8),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
currentY -= spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'combined-page.pdf');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'An error occurred while combining pages.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let currentY = totalHeight;
|
||||||
|
for (let i = 0; i < sourcePages.length; i++) {
|
||||||
|
const sourcePage = sourcePages[i];
|
||||||
|
const { width, height } = sourcePage.getSize();
|
||||||
|
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||||
|
|
||||||
|
currentY -= height;
|
||||||
|
const x = (maxWidth - width) / 2;
|
||||||
|
|
||||||
|
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
||||||
|
|
||||||
|
if (addSeparator && i < sourcePages.length - 1) {
|
||||||
|
const lineY = currentY - spacing / 2;
|
||||||
|
newPage.drawLine({
|
||||||
|
start: { x: 0, y: lineY },
|
||||||
|
end: { x: maxWidth, y: lineY },
|
||||||
|
thickness: 0.5,
|
||||||
|
color: rgb(0.8, 0.8, 0.8),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
currentY -= spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdfBytes = await newDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'combined-page.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'An error occurred while combining pages.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+189
-152
@@ -1,13 +1,13 @@
|
|||||||
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,
|
||||||
pdfDoc2: null,
|
pdfDoc2: null,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
viewMode: 'overlay',
|
viewMode: 'overlay',
|
||||||
isSyncScroll: true,
|
isSyncScroll: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,110 +17,137 @@ 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(
|
||||||
const page = await pdfDoc.getPage(pageNum);
|
pdfDoc: any,
|
||||||
|
pageNum: any,
|
||||||
|
canvas: any,
|
||||||
|
container: any
|
||||||
|
) {
|
||||||
|
const page = await pdfDoc.getPage(pageNum);
|
||||||
|
|
||||||
// Calculate scale to fit the container width.
|
// Calculate scale to fit the container width.
|
||||||
const containerWidth = container.clientWidth - 2; // Subtract border width
|
const containerWidth = container.clientWidth - 2; // Subtract border width
|
||||||
const viewport = page.getViewport({ scale: 1.0 });
|
const viewport = page.getViewport({ scale: 1.0 });
|
||||||
const scale = containerWidth / viewport.width;
|
const scale = containerWidth / viewport.width;
|
||||||
const scaledViewport = page.getViewport({ scale: scale });
|
const scaledViewport = page.getViewport({ scale: scale });
|
||||||
|
|
||||||
canvas.width = scaledViewport.width;
|
canvas.width = scaledViewport.width;
|
||||||
canvas.height = scaledViewport.height;
|
canvas.height = scaledViewport.height;
|
||||||
|
|
||||||
await page.render({
|
await page.render({
|
||||||
canvasContext: canvas.getContext('2d'),
|
canvasContext: canvas.getContext('2d'),
|
||||||
viewport: scaledViewport,
|
viewport: scaledViewport,
|
||||||
}).promise;
|
}).promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function renderBothPages() {
|
async function renderBothPages() {
|
||||||
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
||||||
|
|
||||||
showLoader(`Loading page ${state.currentPage}...`);
|
showLoader(`Loading page ${state.currentPage}...`);
|
||||||
|
|
||||||
const canvas1 = document.getElementById('canvas-compare-1');
|
const canvas1 = document.getElementById('canvas-compare-1');
|
||||||
const canvas2 = document.getElementById('canvas-compare-2');
|
const canvas2 = document.getElementById('canvas-compare-2');
|
||||||
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 wrapper = document.getElementById('compare-viewer-wrapper');
|
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||||
|
|
||||||
// Determine the correct container based on the view mode
|
// Determine the correct container based on the view mode
|
||||||
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
|
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
|
||||||
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();
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNavControls() {
|
function updateNavControls() {
|
||||||
const maxPages = Math.max(state.pdfDoc1?.numPages || 0, state.pdfDoc2?.numPages || 0);
|
const maxPages = Math.max(
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
state.pdfDoc1?.numPages || 0,
|
||||||
document.getElementById('current-page-display-compare').textContent = state.currentPage;
|
state.pdfDoc2?.numPages || 0
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
);
|
||||||
document.getElementById('total-pages-display-compare').textContent = maxPages;
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
// @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('current-page-display-compare').textContent =
|
||||||
document.getElementById('prev-page-compare').disabled = state.currentPage <= 1;
|
state.currentPage;
|
||||||
// @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(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
document.getElementById('next-page-compare').disabled = state.currentPage >= 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
|
||||||
|
document.getElementById('prev-page-compare').disabled =
|
||||||
|
state.currentPage <= 1;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
|
document.getElementById('next-page-compare').disabled =
|
||||||
|
state.currentPage >= maxPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
||||||
const fileInput = document.getElementById(inputId);
|
const fileInput = document.getElementById(inputId);
|
||||||
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 = '';
|
||||||
|
|
||||||
// 2. Create the icon element
|
// 2. Create the icon element
|
||||||
const icon = document.createElement('i');
|
const icon = document.createElement('i');
|
||||||
icon.setAttribute('data-lucide', 'check-circle');
|
icon.setAttribute('data-lucide', 'check-circle');
|
||||||
icon.className = 'w-10 h-10 mb-3 text-green-500';
|
icon.className = 'w-10 h-10 mb-3 text-green-500';
|
||||||
|
|
||||||
// 3. Create the paragraph element for the file name
|
// 3. Create the paragraph element for the file name
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.className = 'text-sm text-gray-300 truncate';
|
p.className = 'text-sm text-gray-300 truncate';
|
||||||
|
|
||||||
// 4. Set the file name safely using textContent
|
// 4. Set the file name safely using textContent
|
||||||
p.textContent = file.name;
|
p.textContent = file.name;
|
||||||
|
|
||||||
// 5. Append the safe elements to the container
|
// 5. Append the safe elements to the container
|
||||||
displayDiv.append(icon, p);
|
displayDiv.append(icon, p);
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader(`Loading ${file.name}...`);
|
showLoader(`Loading ${file.name}...`);
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
|
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
|
||||||
|
|
||||||
if (state.pdfDoc1 && state.pdfDoc2) {
|
if (state.pdfDoc1 && state.pdfDoc2) {
|
||||||
document.getElementById('compare-viewer').classList.remove('hidden');
|
document.getElementById('compare-viewer').classList.remove('hidden');
|
||||||
state.currentPage = 1;
|
state.currentPage = 1;
|
||||||
await renderBothPages();
|
await renderBothPages();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showAlert('Error', 'Could not load PDF. It may be corrupt or password-protected.');
|
showAlert(
|
||||||
console.error(e);
|
'Error',
|
||||||
} finally {
|
'Could not load PDF. It may be corrupt or password-protected.'
|
||||||
hideLoader();
|
);
|
||||||
}
|
console.error(e);
|
||||||
};
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
// @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]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,81 +155,91 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
|||||||
* @param {'overlay' | 'side-by-side'} mode
|
* @param {'overlay' | 'side-by-side'} mode
|
||||||
*/
|
*/
|
||||||
function setViewMode(mode: any) {
|
function setViewMode(mode: any) {
|
||||||
state.viewMode = mode;
|
state.viewMode = mode;
|
||||||
const wrapper = document.getElementById('compare-viewer-wrapper');
|
const wrapper = document.getElementById('compare-viewer-wrapper');
|
||||||
const overlayControls = document.getElementById('overlay-controls');
|
const overlayControls = document.getElementById('overlay-controls');
|
||||||
const sideControls = document.getElementById('side-by-side-controls');
|
const sideControls = document.getElementById('side-by-side-controls');
|
||||||
const btnOverlay = document.getElementById('view-mode-overlay');
|
const btnOverlay = document.getElementById('view-mode-overlay');
|
||||||
const btnSide = document.getElementById('view-mode-side');
|
const btnSide = document.getElementById('view-mode-side');
|
||||||
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');
|
sideControls.classList.add('hidden');
|
||||||
sideControls.classList.add('hidden');
|
btnOverlay.classList.add('bg-indigo-600');
|
||||||
btnOverlay.classList.add('bg-indigo-600');
|
btnSide.classList.remove('bg-indigo-600');
|
||||||
btnSide.classList.remove('bg-indigo-600');
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
canvas2.style.opacity = opacitySlider.value;
|
||||||
canvas2.style.opacity = opacitySlider.value;
|
} else {
|
||||||
} else {
|
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
|
||||||
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
|
overlayControls.classList.add('hidden');
|
||||||
overlayControls.classList.add('hidden');
|
sideControls.classList.remove('hidden');
|
||||||
sideControls.classList.remove('hidden');
|
btnOverlay.classList.remove('bg-indigo-600');
|
||||||
btnOverlay.classList.remove('bg-indigo-600');
|
btnSide.classList.add('bg-indigo-600');
|
||||||
btnSide.classList.add('bg-indigo-600');
|
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
|
||||||
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
|
canvas2.style.opacity = '1';
|
||||||
canvas2.style.opacity = '1';
|
}
|
||||||
}
|
renderBothPages();
|
||||||
renderBothPages();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setupCompareTool() {
|
export function setupCompareTool() {
|
||||||
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
|
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
|
||||||
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--;
|
||||||
document.getElementById('next-page-compare').addEventListener('click', () => {
|
renderBothPages();
|
||||||
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
}
|
||||||
if (state.currentPage < maxPages) { state.currentPage++; renderBothPages(); }
|
});
|
||||||
});
|
document.getElementById('next-page-compare').addEventListener('click', () => {
|
||||||
|
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
||||||
|
if (state.currentPage < maxPages) {
|
||||||
|
state.currentPage++;
|
||||||
|
renderBothPages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('view-mode-overlay').addEventListener('click', () => setViewMode('overlay'));
|
document
|
||||||
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 = '';
|
||||||
// @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
|
||||||
canvas2.style.opacity = e.target.value;
|
canvas2.style.opacity = e.target.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+301
-236
@@ -1,279 +1,344 @@
|
|||||||
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) {
|
||||||
const base64 = dataUrl.split(',')[1];
|
const base64 = dataUrl.split(',')[1];
|
||||||
const binaryString = atob(base64);
|
const binaryString = atob(base64);
|
||||||
const len = binaryString.length;
|
const len = binaryString.length;
|
||||||
const bytes = new Uint8Array(len);
|
const bytes = new Uint8Array(len);
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
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, {
|
||||||
const pages = pdfDoc.getPages();
|
ignoreEncryption: true,
|
||||||
|
});
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
if (settings.removeMetadata) {
|
if (settings.removeMetadata) {
|
||||||
try {
|
try {
|
||||||
pdfDoc.setTitle('');
|
pdfDoc.setTitle('');
|
||||||
pdfDoc.setAuthor('');
|
pdfDoc.setAuthor('');
|
||||||
pdfDoc.setSubject('');
|
pdfDoc.setSubject('');
|
||||||
pdfDoc.setKeywords([]);
|
pdfDoc.setKeywords([]);
|
||||||
pdfDoc.setCreator('');
|
pdfDoc.setCreator('');
|
||||||
pdfDoc.setProducer('');
|
pdfDoc.setProducer('');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Could not remove metadata:', e);
|
console.warn('Could not remove metadata:', e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < pages.length; i++) {
|
for (let i = 0; i < pages.length; i++) {
|
||||||
const page = pages[i];
|
const page = pages[i];
|
||||||
const resources = page.node.Resources();
|
const resources = page.node.Resources();
|
||||||
if (!resources) continue;
|
if (!resources) continue;
|
||||||
|
|
||||||
const xobjects = resources.lookup(PDFName.of('XObject'));
|
const xobjects = resources.lookup(PDFName.of('XObject'));
|
||||||
if (!(xobjects instanceof PDFDict)) continue;
|
if (!(xobjects instanceof PDFDict)) continue;
|
||||||
|
|
||||||
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')) as PDFNumber).asNumber()
|
stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
|
||||||
: 0;
|
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
|
||||||
const height = stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
|
: 0;
|
||||||
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
|
const height =
|
||||||
: 0;
|
stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
|
||||||
const bitsPerComponent = stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
|
||||||
? (stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber).asNumber()
|
: 0;
|
||||||
: 8;
|
const bitsPerComponent =
|
||||||
|
stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
||||||
|
? (
|
||||||
|
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
|
||||||
|
).asNumber()
|
||||||
|
: 8;
|
||||||
|
|
||||||
if (width > 0 && height > 0) {
|
if (width > 0 && height > 0) {
|
||||||
let newWidth = width;
|
let newWidth = width;
|
||||||
let newHeight = height;
|
let newHeight = height;
|
||||||
|
|
||||||
const scaleFactor = settings.scaleFactor || 1.0;
|
const scaleFactor = settings.scaleFactor || 1.0;
|
||||||
newWidth = Math.floor(width * scaleFactor);
|
newWidth = Math.floor(width * scaleFactor);
|
||||||
newHeight = Math.floor(height * scaleFactor);
|
newHeight = Math.floor(height * scaleFactor);
|
||||||
|
|
||||||
if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) {
|
if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) {
|
||||||
const aspectRatio = newWidth / newHeight;
|
const aspectRatio = newWidth / newHeight;
|
||||||
if (newWidth > newHeight) {
|
if (newWidth > newHeight) {
|
||||||
newWidth = Math.min(newWidth, settings.maxWidth);
|
newWidth = Math.min(newWidth, settings.maxWidth);
|
||||||
newHeight = newWidth / aspectRatio;
|
newHeight = newWidth / aspectRatio;
|
||||||
} else {
|
} else {
|
||||||
newHeight = Math.min(newHeight, settings.maxHeight);
|
newHeight = Math.min(newHeight, settings.maxHeight);
|
||||||
newWidth = newHeight * aspectRatio;
|
newWidth = newHeight * aspectRatio;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const minDim = settings.minDimension || 50;
|
|
||||||
if (newWidth < minDim || newHeight < minDim) continue;
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
canvas.width = Math.floor(newWidth);
|
|
||||||
canvas.height = Math.floor(newHeight);
|
|
||||||
|
|
||||||
const img = new Image();
|
|
||||||
const imageUrl = URL.createObjectURL(new Blob([new Uint8Array(imageBytes)]));
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
img.onload = resolve;
|
|
||||||
img.onerror = reject;
|
|
||||||
img.src = imageUrl;
|
|
||||||
});
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = settings.smoothing !== false;
|
|
||||||
ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium';
|
|
||||||
|
|
||||||
if (settings.grayscale) {
|
|
||||||
ctx.filter = 'grayscale(100%)';
|
|
||||||
} else if (settings.contrast) {
|
|
||||||
ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
let bestBytes = null;
|
|
||||||
let bestSize = imageBytes.length;
|
|
||||||
|
|
||||||
const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality);
|
|
||||||
const jpegBytes = dataUrlToBytes(jpegDataUrl);
|
|
||||||
if (jpegBytes.length < bestSize) {
|
|
||||||
bestBytes = jpegBytes;
|
|
||||||
bestSize = jpegBytes.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settings.tryWebP) {
|
|
||||||
try {
|
|
||||||
const webpDataUrl = canvas.toDataURL('image/webp', settings.quality);
|
|
||||||
const webpBytes = dataUrlToBytes(webpDataUrl);
|
|
||||||
if (webpBytes.length < bestSize) {
|
|
||||||
bestBytes = webpBytes;
|
|
||||||
bestSize = webpBytes.length;
|
|
||||||
}
|
|
||||||
} catch (e) { /* WebP not supported */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
|
|
||||||
(stream as any).contents = bestBytes;
|
|
||||||
stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize));
|
|
||||||
stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width));
|
|
||||||
stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height));
|
|
||||||
stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode'));
|
|
||||||
stream.dict.delete(PDFName.of('DecodeParms'));
|
|
||||||
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
|
|
||||||
|
|
||||||
if (settings.grayscale) {
|
|
||||||
stream.dict.set(PDFName.of('ColorSpace'), PDFName.of('DeviceGray'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
URL.revokeObjectURL(imageUrl);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Skipping an uncompressible image in smart mode:', error);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const minDim = settings.minDimension || 50;
|
||||||
|
if (newWidth < minDim || newHeight < minDim) continue;
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
canvas.width = Math.floor(newWidth);
|
||||||
|
canvas.height = Math.floor(newHeight);
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
const imageUrl = URL.createObjectURL(
|
||||||
|
new Blob([new Uint8Array(imageBytes)])
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
img.onload = resolve;
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = imageUrl;
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.imageSmoothingEnabled = settings.smoothing !== false;
|
||||||
|
ctx.imageSmoothingQuality = settings.smoothingQuality || 'medium';
|
||||||
|
|
||||||
|
if (settings.grayscale) {
|
||||||
|
ctx.filter = 'grayscale(100%)';
|
||||||
|
} else if (settings.contrast) {
|
||||||
|
ctx.filter = `contrast(${settings.contrast}) brightness(${settings.brightness || 1})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
let bestBytes = null;
|
||||||
|
let bestSize = imageBytes.length;
|
||||||
|
|
||||||
|
const jpegDataUrl = canvas.toDataURL('image/jpeg', settings.quality);
|
||||||
|
const jpegBytes = dataUrlToBytes(jpegDataUrl);
|
||||||
|
if (jpegBytes.length < bestSize) {
|
||||||
|
bestBytes = jpegBytes;
|
||||||
|
bestSize = jpegBytes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.tryWebP) {
|
||||||
|
try {
|
||||||
|
const webpDataUrl = canvas.toDataURL(
|
||||||
|
'image/webp',
|
||||||
|
settings.quality
|
||||||
|
);
|
||||||
|
const webpBytes = dataUrlToBytes(webpDataUrl);
|
||||||
|
if (webpBytes.length < bestSize) {
|
||||||
|
bestBytes = webpBytes;
|
||||||
|
bestSize = webpBytes.length;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* WebP not supported */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
|
||||||
|
(stream as any).contents = bestBytes;
|
||||||
|
stream.dict.set(PDFName.of('Length'), PDFNumber.of(bestSize));
|
||||||
|
stream.dict.set(PDFName.of('Width'), PDFNumber.of(canvas.width));
|
||||||
|
stream.dict.set(PDFName.of('Height'), PDFNumber.of(canvas.height));
|
||||||
|
stream.dict.set(PDFName.of('Filter'), PDFName.of('DCTDecode'));
|
||||||
|
stream.dict.delete(PDFName.of('DecodeParms'));
|
||||||
|
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
|
||||||
|
|
||||||
|
if (settings.grayscale) {
|
||||||
|
stream.dict.set(
|
||||||
|
PDFName.of('ColorSpace'),
|
||||||
|
PDFName.of('DeviceGray')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
URL.revokeObjectURL(imageUrl);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Skipping an uncompressible image in smart mode:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||||
const newPdfDoc = await PDFDocument.create();
|
const newPdfDoc = await PDFDocument.create();
|
||||||
|
|
||||||
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
|
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
|
||||||
const page = await pdfJsDoc.getPage(i);
|
const page = await pdfJsDoc.getPage(i);
|
||||||
const viewport = page.getViewport({ scale: settings.scale });
|
const viewport = page.getViewport({ scale: settings.scale });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
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) =>
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
canvas.toBlob(resolve, 'image/jpeg', settings.quality)
|
||||||
const jpegBytes = await jpegBlob.arrayBuffer();
|
);
|
||||||
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
const jpegBytes = await jpegBlob.arrayBuffer();
|
||||||
newPage.drawImage(jpegImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
||||||
}
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
return await newPdfDoc.save();
|
newPage.drawImage(jpegImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await newPdfDoc.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function compress() {
|
export async function compress() {
|
||||||
// @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 level = document.getElementById('compression-level').value;
|
const level = document.getElementById('compression-level').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 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,
|
||||||
'high-quality': {
|
maxWidth: 1800,
|
||||||
smart: { quality: 0.70, threshold: 0.98, maxWidth: 2500, maxHeight: 2500, skipSize: 5000 },
|
maxHeight: 1800,
|
||||||
legacy: { scale: 2.0, quality: 0.9 }
|
skipSize: 3000,
|
||||||
},
|
},
|
||||||
'small-size': {
|
legacy: { scale: 1.5, quality: 0.6 },
|
||||||
smart: { quality: 0.3, threshold: 0.95, maxWidth: 1200, maxHeight: 1200, skipSize: 2000 },
|
},
|
||||||
legacy: { scale: 1.2, quality: 0.4 }
|
'high-quality': {
|
||||||
},
|
smart: {
|
||||||
'extreme': {
|
quality: 0.7,
|
||||||
smart: { quality: 0.1, threshold: 0.95, maxWidth: 1000, maxHeight: 1000, skipSize: 1000 },
|
threshold: 0.98,
|
||||||
legacy: { scale: 1.0, quality: 0.2 }
|
maxWidth: 2500,
|
||||||
}
|
maxHeight: 2500,
|
||||||
};
|
skipSize: 5000,
|
||||||
|
},
|
||||||
|
legacy: { scale: 2.0, quality: 0.9 },
|
||||||
|
},
|
||||||
|
'small-size': {
|
||||||
|
smart: {
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
||||||
const legacySettings = settings[level].legacy;
|
const legacySettings = settings[level].legacy;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const originalFile = state.files[0];
|
const originalFile = state.files[0];
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
|
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
|
||||||
|
|
||||||
let resultBytes;
|
let resultBytes;
|
||||||
let usedMethod;
|
let usedMethod;
|
||||||
|
|
||||||
if (algorithm === 'vector') {
|
if (algorithm === 'vector') {
|
||||||
showLoader('Running Vector (Smart) compression...');
|
showLoader('Running Vector (Smart) compression...');
|
||||||
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
||||||
usedMethod = 'Vector';
|
usedMethod = 'Vector';
|
||||||
} else if (algorithm === 'photon') {
|
} else if (algorithm === 'photon') {
|
||||||
showLoader('Running Photon (Rasterize) compression...');
|
showLoader('Running Photon (Rasterize) compression...');
|
||||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
||||||
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(
|
||||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
arrayBuffer,
|
||||||
usedMethod = 'Photon (Automatic)';
|
legacySettings
|
||||||
}
|
);
|
||||||
}
|
usedMethod = 'Photon (Automatic)';
|
||||||
|
}
|
||||||
const originalSize = formatBytes(originalFile.size);
|
|
||||||
const compressedSize = formatBytes(resultBytes.length);
|
|
||||||
const savings = originalFile.size - resultBytes.length;
|
|
||||||
const savingsPercent = savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
|
||||||
|
|
||||||
if (savings > 0) {
|
|
||||||
showAlert(
|
|
||||||
'Compression Complete',
|
|
||||||
`Method: **${usedMethod}**. ` +
|
|
||||||
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
showAlert(
|
|
||||||
'Compression Finished',
|
|
||||||
`Method: **${usedMethod}**. ` +
|
|
||||||
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
|
|
||||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
|
||||||
'warning'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadFile(new Blob([resultBytes], { type: 'application/pdf' }), 'compressed-final.pdf');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
|
||||||
showAlert('Error', `An error occurred during compression. Error: ${e.message}`, 'error');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const originalSize = formatBytes(originalFile.size);
|
||||||
|
const compressedSize = formatBytes(resultBytes.length);
|
||||||
|
const savings = originalFile.size - resultBytes.length;
|
||||||
|
const savingsPercent =
|
||||||
|
savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
|
if (savings > 0) {
|
||||||
|
showAlert(
|
||||||
|
'Compression Complete',
|
||||||
|
`Method: **${usedMethod}**. ` +
|
||||||
|
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showAlert(
|
||||||
|
'Compression Finished',
|
||||||
|
`Method: **${usedMethod}**. ` +
|
||||||
|
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
|
||||||
|
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(
|
||||||
|
new Blob([resultBytes], { type: 'application/pdf' }),
|
||||||
|
'compressed-final.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
`An error occurred during compression. Error: ${e.message}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+269
-230
@@ -1,36 +1,35 @@
|
|||||||
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,
|
||||||
currentPageNum: 1,
|
currentPageNum: 1,
|
||||||
cropper: null,
|
cropper: null,
|
||||||
originalPdfBytes: null,
|
originalPdfBytes: null,
|
||||||
cropperImageElement: null,
|
cropperImageElement: null,
|
||||||
pageCrops: {},
|
pageCrops: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the current crop data to the state object.
|
* Saves the current crop data to the state object.
|
||||||
*/
|
*/
|
||||||
function saveCurrentCrop() {
|
function saveCurrentCrop() {
|
||||||
if (cropperState.cropper) {
|
if (cropperState.cropper) {
|
||||||
const currentCrop = cropperState.cropper.getData(true);
|
const currentCrop = cropperState.cropper.getData(true);
|
||||||
const imageData = cropperState.cropper.getImageData();
|
const imageData = cropperState.cropper.getImageData();
|
||||||
const cropPercentages = {
|
const cropPercentages = {
|
||||||
x: currentCrop.x / imageData.naturalWidth,
|
x: currentCrop.x / imageData.naturalWidth,
|
||||||
y: currentCrop.y / imageData.naturalHeight,
|
y: currentCrop.y / imageData.naturalHeight,
|
||||||
width: currentCrop.width / imageData.naturalWidth,
|
width: currentCrop.width / imageData.naturalWidth,
|
||||||
height: currentCrop.height / imageData.naturalHeight,
|
height: currentCrop.height / imageData.naturalHeight,
|
||||||
};
|
};
|
||||||
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
|
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,60 +37,60 @@ function saveCurrentCrop() {
|
|||||||
* @param {number} num The page number to render.
|
* @param {number} num The page number to render.
|
||||||
*/
|
*/
|
||||||
async function displayPageAsImage(num: any) {
|
async function displayPageAsImage(num: any) {
|
||||||
showLoader(`Rendering Page ${num}...`);
|
showLoader(`Rendering Page ${num}...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = await cropperState.pdfDoc.getPage(num);
|
const page = await cropperState.pdfDoc.getPage(num);
|
||||||
const viewport = page.getViewport({ scale: 2.5 });
|
const viewport = page.getViewport({ scale: 2.5 });
|
||||||
|
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
const tempCtx = tempCanvas.getContext('2d');
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
tempCanvas.width = viewport.width;
|
tempCanvas.width = viewport.width;
|
||||||
tempCanvas.height = viewport.height;
|
tempCanvas.height = viewport.height;
|
||||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||||
|
|
||||||
if (cropperState.cropper) {
|
if (cropperState.cropper) {
|
||||||
cropperState.cropper.destroy();
|
cropperState.cropper.destroy();
|
||||||
}
|
|
||||||
|
|
||||||
const image = document.createElement('img');
|
|
||||||
image.src = tempCanvas.toDataURL('image/png');
|
|
||||||
document.getElementById('cropper-container').innerHTML = '';
|
|
||||||
document.getElementById('cropper-container').appendChild(image);
|
|
||||||
|
|
||||||
image.onload = () => {
|
|
||||||
cropperState.cropper = new Cropper(image, {
|
|
||||||
viewMode: 1,
|
|
||||||
background: false,
|
|
||||||
autoCropArea: 0.8,
|
|
||||||
responsive: true,
|
|
||||||
rotatable: false,
|
|
||||||
zoomable: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restore saved crop data for this page
|
|
||||||
const savedCrop = cropperState.pageCrops[num];
|
|
||||||
if (savedCrop) {
|
|
||||||
const imageData = cropperState.cropper.getImageData();
|
|
||||||
const cropData = {
|
|
||||||
x: savedCrop.x * imageData.naturalWidth,
|
|
||||||
y: savedCrop.y * imageData.naturalHeight,
|
|
||||||
width: savedCrop.width * imageData.naturalWidth,
|
|
||||||
height: savedCrop.height * imageData.naturalHeight,
|
|
||||||
};
|
|
||||||
cropperState.cropper.setData(cropData);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePageInfo();
|
|
||||||
enableControls();
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Ready', 'Please select an area to crop.');
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error rendering page:", error);
|
|
||||||
showAlert('Error', 'Failed to render page.');
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const image = document.createElement('img');
|
||||||
|
image.src = tempCanvas.toDataURL('image/png');
|
||||||
|
document.getElementById('cropper-container').innerHTML = '';
|
||||||
|
document.getElementById('cropper-container').appendChild(image);
|
||||||
|
|
||||||
|
image.onload = () => {
|
||||||
|
cropperState.cropper = new Cropper(image, {
|
||||||
|
viewMode: 1,
|
||||||
|
background: false,
|
||||||
|
autoCropArea: 0.8,
|
||||||
|
responsive: true,
|
||||||
|
rotatable: false,
|
||||||
|
zoomable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore saved crop data for this page
|
||||||
|
const savedCrop = cropperState.pageCrops[num];
|
||||||
|
if (savedCrop) {
|
||||||
|
const imageData = cropperState.cropper.getImageData();
|
||||||
|
const cropData = {
|
||||||
|
x: savedCrop.x * imageData.naturalWidth,
|
||||||
|
y: savedCrop.y * imageData.naturalHeight,
|
||||||
|
width: savedCrop.width * imageData.naturalWidth,
|
||||||
|
height: savedCrop.height * imageData.naturalHeight,
|
||||||
|
};
|
||||||
|
cropperState.cropper.setData(cropData);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePageInfo();
|
||||||
|
enableControls();
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Ready', 'Please select an area to crop.');
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering page:', error);
|
||||||
|
showAlert('Error', 'Failed to render page.');
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,211 +98,251 @@ async function displayPageAsImage(num: any) {
|
|||||||
* @param {number} offset -1 for previous, 1 for next.
|
* @param {number} offset -1 for previous, 1 for next.
|
||||||
*/
|
*/
|
||||||
async function changePage(offset: any) {
|
async function changePage(offset: any) {
|
||||||
// Save the current page's crop before changing
|
// Save the current page's crop before changing
|
||||||
saveCurrentCrop();
|
saveCurrentCrop();
|
||||||
|
|
||||||
const newPageNum = cropperState.currentPageNum + offset;
|
const newPageNum = cropperState.currentPageNum + offset;
|
||||||
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
|
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
|
||||||
cropperState.currentPageNum = newPageNum;
|
cropperState.currentPageNum = newPageNum;
|
||||||
await displayPageAsImage(cropperState.currentPageNum);
|
await displayPageAsImage(cropperState.currentPageNum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 =
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
cropperState.currentPageNum <= 1;
|
||||||
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('next-page').disabled =
|
||||||
document.getElementById('crop-button').disabled = false;
|
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
|
||||||
|
document.getElementById('crop-button').disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a non-destructive crop by updating the page's crop box.
|
* Performs a non-destructive crop by updating the page's crop box.
|
||||||
*/
|
*/
|
||||||
async function performMetadataCrop(pdfToModify: any, cropData: any) {
|
async function performMetadataCrop(pdfToModify: any, cropData: any) {
|
||||||
for (const pageNum in cropData) {
|
for (const pageNum in cropData) {
|
||||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||||
const page = pdfToModify.getPages()[pageNum - 1];
|
const page = pdfToModify.getPages()[pageNum - 1];
|
||||||
const { width: pageWidth, height: pageHeight } = page.getSize();
|
const { width: pageWidth, height: pageHeight } = page.getSize();
|
||||||
const rotation = page.getRotation().angle;
|
const rotation = page.getRotation().angle;
|
||||||
const crop = cropData[pageNum];
|
const crop = cropData[pageNum];
|
||||||
|
|
||||||
const visualPdfWidth = pageWidth * crop.width;
|
const visualPdfWidth = pageWidth * crop.width;
|
||||||
const visualPdfHeight = pageHeight * crop.height;
|
const visualPdfHeight = pageHeight * crop.height;
|
||||||
const visualPdfX = pageWidth * crop.x;
|
const visualPdfX = pageWidth * crop.x;
|
||||||
const visualPdfY = pageHeight * crop.y;
|
const visualPdfY = pageHeight * crop.y;
|
||||||
|
|
||||||
let finalX, finalY, finalWidth, finalHeight;
|
let finalX, finalY, finalWidth, finalHeight;
|
||||||
switch (rotation) {
|
switch (rotation) {
|
||||||
case 90:
|
case 90:
|
||||||
finalX = visualPdfY;
|
finalX = visualPdfY;
|
||||||
finalY = pageWidth - visualPdfX - visualPdfWidth;
|
finalY = pageWidth - visualPdfX - visualPdfWidth;
|
||||||
finalWidth = visualPdfHeight;
|
finalWidth = visualPdfHeight;
|
||||||
finalHeight = visualPdfWidth;
|
finalHeight = visualPdfWidth;
|
||||||
break;
|
break;
|
||||||
case 180:
|
case 180:
|
||||||
finalX = pageWidth - visualPdfX - visualPdfWidth;
|
finalX = pageWidth - visualPdfX - visualPdfWidth;
|
||||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
||||||
finalWidth = visualPdfWidth;
|
finalWidth = visualPdfWidth;
|
||||||
finalHeight = visualPdfHeight;
|
finalHeight = visualPdfHeight;
|
||||||
break;
|
break;
|
||||||
case 270:
|
case 270:
|
||||||
finalX = pageHeight - visualPdfY - visualPdfHeight;
|
finalX = pageHeight - visualPdfY - visualPdfHeight;
|
||||||
finalY = visualPdfX;
|
finalY = visualPdfX;
|
||||||
finalWidth = visualPdfHeight;
|
finalWidth = visualPdfHeight;
|
||||||
finalHeight = visualPdfWidth;
|
finalHeight = visualPdfWidth;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
finalX = visualPdfX;
|
finalX = visualPdfX;
|
||||||
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
finalY = pageHeight - visualPdfY - visualPdfHeight;
|
||||||
finalWidth = visualPdfWidth;
|
finalWidth = visualPdfWidth;
|
||||||
finalHeight = visualPdfHeight;
|
finalHeight = visualPdfHeight;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
|
|
||||||
}
|
}
|
||||||
|
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Performs a destructive crop by flattening the selected area to an image.
|
* Performs a destructive crop by flattening the selected area to an image.
|
||||||
*/
|
*/
|
||||||
async function performFlatteningCrop(cropData: any) {
|
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(
|
||||||
const totalPages = cropperState.pdfDoc.numPages;
|
cropperState.originalPdfBytes
|
||||||
|
);
|
||||||
|
const totalPages = cropperState.pdfDoc.numPages;
|
||||||
|
|
||||||
for (let i = 0; i < totalPages; i++) {
|
for (let i = 0; i < totalPages; i++) {
|
||||||
const pageNum = i + 1;
|
const pageNum = i + 1;
|
||||||
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
|
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
|
||||||
|
|
||||||
if (cropData[pageNum]) {
|
if (cropData[pageNum]) {
|
||||||
const page = await cropperState.pdfDoc.getPage(pageNum);
|
const page = await cropperState.pdfDoc.getPage(pageNum);
|
||||||
const viewport = page.getViewport({ scale: 2.5 });
|
const viewport = page.getViewport({ scale: 2.5 });
|
||||||
|
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
const tempCtx = tempCanvas.getContext('2d');
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
tempCanvas.width = viewport.width;
|
tempCanvas.width = viewport.width;
|
||||||
tempCanvas.height = viewport.height;
|
tempCanvas.height = viewport.height;
|
||||||
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
|
||||||
|
|
||||||
const finalCanvas = document.createElement('canvas');
|
const finalCanvas = document.createElement('canvas');
|
||||||
const finalCtx = finalCanvas.getContext('2d');
|
const finalCtx = finalCanvas.getContext('2d');
|
||||||
const crop = cropData[pageNum];
|
const crop = cropData[pageNum];
|
||||||
const finalWidth = tempCanvas.width * crop.width;
|
const finalWidth = tempCanvas.width * crop.width;
|
||||||
const finalHeight = tempCanvas.height * crop.height;
|
const finalHeight = tempCanvas.height * crop.height;
|
||||||
finalCanvas.width = finalWidth;
|
finalCanvas.width = finalWidth;
|
||||||
finalCanvas.height = finalHeight;
|
finalCanvas.height = finalHeight;
|
||||||
|
|
||||||
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) =>
|
||||||
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
finalCanvas.toBlob((blob) => blob.arrayBuffer().then(res), 'image/png')
|
||||||
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
);
|
||||||
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight });
|
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||||
} else {
|
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
||||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
|
newPage.drawImage(embeddedImage, {
|
||||||
newPdfDoc.addPage(copiedPage);
|
x: 0,
|
||||||
}
|
y: 0,
|
||||||
|
width: finalWidth,
|
||||||
|
height: finalHeight,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [
|
||||||
|
i,
|
||||||
|
]);
|
||||||
|
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;
|
||||||
|
|
||||||
// Clear pageCrops on new file upload
|
// Clear pageCrops on new file upload
|
||||||
cropperState.pageCrops = {};
|
cropperState.pageCrops = {};
|
||||||
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
|
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
|
||||||
cropperState.originalPdfBytes = arrayBuffer;
|
cropperState.originalPdfBytes = arrayBuffer;
|
||||||
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
|
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
|
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
|
||||||
|
|
||||||
cropperState.pdfDoc = await loadingTask.promise;
|
cropperState.pdfDoc = await loadingTask.promise;
|
||||||
cropperState.currentPageNum = 1;
|
cropperState.currentPageNum = 1;
|
||||||
|
|
||||||
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
|
||||||
// Get the last known crop from the active page before processing
|
.getElementById('crop-button')
|
||||||
saveCurrentCrop();
|
.addEventListener('click', async () => {
|
||||||
|
// Get the last known crop from the active page before processing
|
||||||
|
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 =
|
||||||
if (!currentCrop) {
|
cropperState.pageCrops[cropperState.currentPageNum];
|
||||||
showAlert('No Crop Area', 'Please select an area to crop first.');
|
if (!currentCrop) {
|
||||||
return;
|
showAlert('No Crop Area', 'Please select an area to crop first.');
|
||||||
}
|
return;
|
||||||
// Apply the active page's crop to all pages
|
}
|
||||||
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
|
// Apply the active page's crop to all pages
|
||||||
finalCropData[i] = currentCrop;
|
for (let i = 1; i <= cropperState.pdfDoc.numPages; i++) {
|
||||||
}
|
finalCropData[i] = currentCrop;
|
||||||
} else {
|
}
|
||||||
// If not applying to all, only process pages with saved crops
|
} else {
|
||||||
finalCropData = Object.keys(cropperState.pageCrops).reduce((obj, key) => {
|
// If not applying to all, only process pages with saved crops
|
||||||
obj[key] = cropperState.pageCrops[key];
|
finalCropData = Object.keys(cropperState.pageCrops).reduce(
|
||||||
return obj;
|
(obj, key) => {
|
||||||
}, {});
|
obj[key] = cropperState.pageCrops[key];
|
||||||
}
|
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(
|
||||||
return;
|
'No Crop Area',
|
||||||
}
|
'Please select an area on at least one page to crop.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
showLoader('Applying crop...');
|
showLoader('Applying crop...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let finalPdfBytes;
|
let finalPdfBytes;
|
||||||
if (isDestructive) {
|
if (isDestructive) {
|
||||||
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(
|
||||||
await performMetadataCrop(pdfToModify, finalCropData);
|
cropperState.originalPdfBytes
|
||||||
finalPdfBytes = await pdfToModify.save();
|
);
|
||||||
}
|
await performMetadataCrop(pdfToModify, finalCropData);
|
||||||
|
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'
|
||||||
showAlert('Success', 'Crop complete! Your download has started.');
|
: 'standard_crop.pdf';
|
||||||
} catch (e) {
|
downloadFile(
|
||||||
console.error(e);
|
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
||||||
showAlert('Error', 'An error occurred during cropping.');
|
fileName
|
||||||
} finally {
|
);
|
||||||
hideLoader();
|
showAlert('Success', 'Crop complete! Your download has started.');
|
||||||
}
|
} catch (e) {
|
||||||
});
|
console.error(e);
|
||||||
} catch (error) {
|
showAlert('Error', 'An error occurred during cropping.');
|
||||||
console.error("Error setting up cropper tool:", error);
|
} finally {
|
||||||
showAlert('Error', 'Failed to load PDF for cropping.');
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error setting up cropper tool:', error);
|
||||||
|
showAlert('Error', 'Failed to load PDF for cropping.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-54
@@ -3,63 +3,77 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||||
import blobStream from 'blob-stream';
|
import blobStream from 'blob-stream';
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
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
|
// @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 = document.getElementById('password-input').value;
|
const password = document.getElementById('password-input').value;
|
||||||
if (!password.trim()) {
|
if (!password.trim()) {
|
||||||
showAlert('Input Required', 'Please enter the PDF password.');
|
showAlert('Input Required', 'Please enter the PDF password.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoader('Preparing to process...');
|
||||||
|
const pdfData = (await readFileAsArrayBuffer(file)) as ArrayBuffer;
|
||||||
|
const pdf = await pdfjsLib.getDocument({
|
||||||
|
data: pdfData,
|
||||||
|
password: password,
|
||||||
|
}).promise;
|
||||||
|
const numPages = pdf.numPages;
|
||||||
|
const pageImages = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= numPages; i++) {
|
||||||
|
document.getElementById('loader-text').textContent =
|
||||||
|
`Processing page ${i} of ${numPages}...`;
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const viewport = page.getViewport({ scale: 2.0 });
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
await page.render({
|
||||||
|
canvasContext: context,
|
||||||
|
viewport: viewport,
|
||||||
|
canvas: canvas,
|
||||||
|
}).promise;
|
||||||
|
pageImages.push({
|
||||||
|
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
document.getElementById('loader-text').textContent =
|
||||||
showLoader('Preparing to process...');
|
'Building unlocked PDF...';
|
||||||
const pdfData = await readFileAsArrayBuffer(file);
|
const doc = new PDFDocument({
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
size: [pageImages[0].width, pageImages[0].height],
|
||||||
const pdf = await pdfjsLib.getDocument({ data: pdfData, password: password }).promise;
|
});
|
||||||
const numPages = pdf.numPages;
|
const stream = doc.pipe(blobStream());
|
||||||
const pageImages = [];
|
for (let i = 0; i < pageImages.length; i++) {
|
||||||
|
if (i > 0)
|
||||||
for (let i = 1; i <= numPages; i++) {
|
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
doc.image(pageImages[i].data, 0, 0, {
|
||||||
const page = await pdf.getPage(i);
|
width: pageImages[i].width,
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
height: pageImages[i].height,
|
||||||
const canvas = document.createElement('canvas');
|
});
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
|
||||||
pageImages.push({
|
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
|
||||||
width: viewport.width,
|
|
||||||
height: viewport.height
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent = 'Building unlocked PDF...';
|
|
||||||
const doc = new PDFDocument({ size: [pageImages[0].width, pageImages[0].height] });
|
|
||||||
const stream = doc.pipe(blobStream());
|
|
||||||
for (let i = 0; i < pageImages.length; i++) {
|
|
||||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
|
||||||
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
|
|
||||||
}
|
|
||||||
doc.end();
|
|
||||||
|
|
||||||
stream.on('finish', function () {
|
|
||||||
const blob = stream.toBlob('application/pdf');
|
|
||||||
downloadFile(blob, `unlocked-${file.name}`);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Success', 'Decryption complete! Your download has started.');
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error during PDF decryption:", error);
|
|
||||||
hideLoader();
|
|
||||||
if (error.name === 'PasswordException') {
|
|
||||||
showAlert('Incorrect Password', 'The password you entered is incorrect.');
|
|
||||||
} else {
|
|
||||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
doc.end();
|
||||||
|
|
||||||
|
stream.on('finish', function () {
|
||||||
|
const blob = stream.toBlob('application/pdf');
|
||||||
|
downloadFile(blob, `unlocked-${file.name}`);
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Success', 'Decryption complete! Your download has started.');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during PDF decryption:', error);
|
||||||
|
hideLoader();
|
||||||
|
if (error.name === 'PasswordException') {
|
||||||
|
showAlert('Incorrect Password', 'The password you entered is incorrect.');
|
||||||
|
} else {
|
||||||
|
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,53 +5,66 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function deletePages() {
|
export async function deletePages() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const pageInput = document.getElementById('pages-to-delete').value;
|
const pageInput = document.getElementById('pages-to-delete').value;
|
||||||
if (!pageInput) {
|
if (!pageInput) {
|
||||||
showAlert('Invalid Input', 'Please enter page numbers to delete.');
|
showAlert('Invalid Input', 'Please enter page numbers to delete.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
showLoader('Deleting pages...');
|
||||||
|
try {
|
||||||
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
|
const indicesToDelete = new Set();
|
||||||
|
const ranges = pageInput.split(',');
|
||||||
|
|
||||||
|
for (const range of ranges) {
|
||||||
|
const trimmedRange = range.trim();
|
||||||
|
if (trimmedRange.includes('-')) {
|
||||||
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
|
||||||
|
} else {
|
||||||
|
const pageNum = Number(trimmedRange);
|
||||||
|
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||||
|
indicesToDelete.add(pageNum - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
showLoader('Deleting pages...');
|
|
||||||
try {
|
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
|
||||||
const indicesToDelete = new Set();
|
|
||||||
const ranges = pageInput.split(',');
|
|
||||||
|
|
||||||
for (const range of ranges) {
|
if (indicesToDelete.size === 0) {
|
||||||
const trimmedRange = range.trim();
|
showAlert('Invalid Input', 'No valid pages selected for deletion.');
|
||||||
if (trimmedRange.includes('-')) {
|
hideLoader();
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
return;
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
|
||||||
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
|
|
||||||
} else {
|
|
||||||
const pageNum = Number(trimmedRange);
|
|
||||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
|
||||||
indicesToDelete.add(pageNum - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (indicesToDelete.size === 0) {
|
|
||||||
showAlert('Invalid Input', 'No valid pages selected for deletion.');
|
|
||||||
hideLoader();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (indicesToDelete.size >= totalPages) {
|
|
||||||
showAlert('Invalid Input', 'You cannot delete all pages.');
|
|
||||||
hideLoader();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const indicesToKeep = Array.from({ length: totalPages }, (_, i) => i).filter(index => !indicesToDelete.has(index));
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'deleted-pages.pdf');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Could not delete pages.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
if (indicesToDelete.size >= totalPages) {
|
||||||
|
showAlert('Invalid Input', 'You cannot delete all pages.');
|
||||||
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const indicesToKeep = Array.from(
|
||||||
|
{ length: totalPages },
|
||||||
|
(_, i) => i
|
||||||
|
).filter((index) => !indicesToDelete.has(index));
|
||||||
|
const newPdf = await PDFLibDocument.create();
|
||||||
|
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||||
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
|
|
||||||
|
const newPdfBytes = await newPdf.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'deleted-pages.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not delete pages.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+134
-120
@@ -1,39 +1,39 @@
|
|||||||
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() {
|
||||||
const grid = document.getElementById('page-grid');
|
const grid = document.getElementById('page-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||||
|
if (duplicateOrganizeState.sortableInstances.pageGrid) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||||
if (duplicateOrganizeState.sortableInstances.pageGrid) {
|
duplicateOrganizeState.sortableInstances.pageGrid.destroy();
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
}
|
||||||
duplicateOrganizeState.sortableInstances.pageGrid.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
|
||||||
duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, {
|
duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, {
|
||||||
animation: 150,
|
animation: 150,
|
||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
chosenClass: 'sortable-chosen',
|
chosenClass: 'sortable-chosen',
|
||||||
dragClass: 'sortable-drag',
|
dragClass: 'sortable-drag',
|
||||||
filter: '.duplicate-btn, .delete-btn',
|
filter: '.duplicate-btn, .delete-btn',
|
||||||
preventOnFilter: true,
|
preventOnFilter: true,
|
||||||
onStart: function(evt: any) {
|
onStart: function (evt: any) {
|
||||||
evt.item.style.opacity = '0.5';
|
evt.item.style.opacity = '0.5';
|
||||||
},
|
},
|
||||||
onEnd: function(evt: any) {
|
onEnd: function (evt: any) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,124 +41,138 @@ function initializePageGridSortable() {
|
|||||||
* @param {HTMLElement} element The thumbnail element to attach listeners to.
|
* @param {HTMLElement} element The thumbnail element to attach listeners to.
|
||||||
*/
|
*/
|
||||||
function attachEventListeners(element: any) {
|
function attachEventListeners(element: any) {
|
||||||
// Re-number all visible page labels
|
// Re-number all visible page labels
|
||||||
const renumberPages = () => {
|
const renumberPages = () => {
|
||||||
const grid = document.getElementById('page-grid');
|
const grid = document.getElementById('page-grid');
|
||||||
const pages = grid.querySelectorAll('.page-number');
|
const pages = grid.querySelectorAll('.page-number');
|
||||||
pages.forEach((label, index) => {
|
pages.forEach((label, index) => {
|
||||||
// @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'.
|
||||||
label.textContent = index + 1;
|
label.textContent = index + 1;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Duplicate button listener
|
// Duplicate button listener
|
||||||
element.querySelector('.duplicate-btn').addEventListener('click', (e: any) => {
|
element
|
||||||
e.stopPropagation();
|
.querySelector('.duplicate-btn')
|
||||||
const clone = element.cloneNode(true);
|
.addEventListener('click', (e: any) => {
|
||||||
element.after(clone);
|
e.stopPropagation();
|
||||||
attachEventListeners(clone);
|
const clone = element.cloneNode(true);
|
||||||
renumberPages();
|
element.after(clone);
|
||||||
initializePageGridSortable();
|
attachEventListeners(clone);
|
||||||
|
renumberPages();
|
||||||
|
initializePageGridSortable();
|
||||||
});
|
});
|
||||||
|
|
||||||
element.querySelector('.delete-btn').addEventListener('click', (e: any) => {
|
element.querySelector('.delete-btn').addEventListener('click', (e: any) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (document.getElementById('page-grid').children.length > 1) {
|
if (document.getElementById('page-grid').children.length > 1) {
|
||||||
element.remove();
|
element.remove();
|
||||||
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.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function renderDuplicateOrganizeThumbnails() {
|
export async function renderDuplicateOrganizeThumbnails() {
|
||||||
const grid = document.getElementById('page-grid');
|
const grid = document.getElementById('page-grid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
|
|
||||||
showLoader('Rendering page previews...');
|
showLoader('Rendering page previews...');
|
||||||
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'.
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
|
|
||||||
grid.textContent = '';
|
grid.textContent = '';
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||||
const page = await pdfjsDoc.getPage(i);
|
const page = await pdfjsDoc.getPage(i);
|
||||||
const viewport = page.getViewport({ scale: 0.5 });
|
const viewport = page.getViewport({ scale: 0.5 });
|
||||||
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 =
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
||||||
wrapper.dataset.originalPageIndex = i - 1;
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
|
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();
|
||||||
img.className = 'max-w-full max-h-full object-contain';
|
img.className = 'max-w-full max-h-full object-contain';
|
||||||
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 =
|
||||||
pageNumberSpan.textContent = i.toString();
|
'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();
|
||||||
|
|
||||||
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 =
|
||||||
duplicateBtn.title = 'Duplicate Page';
|
'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||||
const duplicateIcon = document.createElement('i');
|
duplicateBtn.title = 'Duplicate Page';
|
||||||
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
const duplicateIcon = document.createElement('i');
|
||||||
duplicateIcon.className = 'w-5 h-5';
|
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
||||||
duplicateBtn.appendChild(duplicateIcon);
|
duplicateIcon.className = 'w-5 h-5';
|
||||||
|
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 =
|
||||||
deleteBtn.title = 'Delete Page';
|
'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||||
const deleteIcon = document.createElement('i');
|
deleteBtn.title = 'Delete Page';
|
||||||
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
const deleteIcon = document.createElement('i');
|
||||||
deleteIcon.className = 'w-5 h-5';
|
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
||||||
deleteBtn.appendChild(deleteIcon);
|
deleteIcon.className = 'w-5 h-5';
|
||||||
|
deleteBtn.appendChild(deleteIcon);
|
||||||
|
|
||||||
controlsDiv.append(duplicateBtn, deleteBtn);
|
controlsDiv.append(duplicateBtn, deleteBtn);
|
||||||
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
|
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
|
||||||
grid.appendChild(wrapper);
|
grid.appendChild(wrapper);
|
||||||
attachEventListeners(wrapper);
|
attachEventListeners(wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
initializePageGridSortable();
|
initializePageGridSortable();
|
||||||
createIcons({icons});
|
createIcons({ icons });
|
||||||
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(
|
||||||
} catch (e) {
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
console.error(e);
|
'organized.pdf'
|
||||||
showAlert('Error', 'Failed to save the new PDF.');
|
);
|
||||||
} finally {
|
} catch (e) {
|
||||||
hideLoader();
|
console.error(e);
|
||||||
}
|
showAlert('Error', 'Failed to save the new PDF.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,73 +1,93 @@
|
|||||||
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...');
|
||||||
try {
|
try {
|
||||||
// @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
|
||||||
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
|
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
|
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
|
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
|
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
|
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @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;
|
||||||
if (creationDate) {
|
if (creationDate) {
|
||||||
state.pdfDoc.setCreationDate(new Date(creationDate));
|
state.pdfDoc.setCreationDate(new Date(creationDate));
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const modDate = document.getElementById('meta-mod-date').value;
|
|
||||||
if (modDate) {
|
|
||||||
state.pdfDoc.setModificationDate(new Date(modDate));
|
|
||||||
} else {
|
|
||||||
state.pdfDoc.setModificationDate(new Date());
|
|
||||||
}
|
|
||||||
|
|
||||||
const infoDict = state.pdfDoc.getInfoDict();
|
|
||||||
const standardKeys = new Set(['Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', 'CreationDate', 'ModDate']);
|
|
||||||
|
|
||||||
const allKeys = infoDict.keys().map((key: any) => key.asString().substring(1)); // Clean keys
|
|
||||||
|
|
||||||
allKeys.forEach((key: any) => {
|
|
||||||
if (!standardKeys.has(key)) {
|
|
||||||
infoDict.delete(PDFName.of(key));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const customKeys = document.querySelectorAll('.custom-meta-key');
|
|
||||||
const customValues = document.querySelectorAll('.custom-meta-value');
|
|
||||||
|
|
||||||
customKeys.forEach((keyInput, index) => {
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
|
||||||
const key = keyInput.value.trim();
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
|
||||||
const value = customValues[index].value.trim();
|
|
||||||
if (key && value) {
|
|
||||||
// Now we add the fields to a clean slate
|
|
||||||
infoDict.set(PDFName.of(key), PDFString.of(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-edited.pdf');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const modDate = document.getElementById('meta-mod-date').value;
|
||||||
|
if (modDate) {
|
||||||
|
state.pdfDoc.setModificationDate(new Date(modDate));
|
||||||
|
} else {
|
||||||
|
state.pdfDoc.setModificationDate(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoDict = state.pdfDoc.getInfoDict();
|
||||||
|
const standardKeys = new Set([
|
||||||
|
'Title',
|
||||||
|
'Author',
|
||||||
|
'Subject',
|
||||||
|
'Keywords',
|
||||||
|
'Creator',
|
||||||
|
'Producer',
|
||||||
|
'CreationDate',
|
||||||
|
'ModDate',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allKeys = infoDict
|
||||||
|
.keys()
|
||||||
|
.map((key: any) => key.asString().substring(1)); // Clean keys
|
||||||
|
|
||||||
|
allKeys.forEach((key: any) => {
|
||||||
|
if (!standardKeys.has(key)) {
|
||||||
|
infoDict.delete(PDFName.of(key));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const customKeys = document.querySelectorAll('.custom-meta-key');
|
||||||
|
const customValues = document.querySelectorAll('.custom-meta-value');
|
||||||
|
|
||||||
|
customKeys.forEach((keyInput, index) => {
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||||
|
const key = keyInput.value.trim();
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||||
|
const value = customValues[index].value.trim();
|
||||||
|
if (key && value) {
|
||||||
|
// Now we add the fields to a clean slate
|
||||||
|
infoDict.set(PDFName.of(key), PDFString.of(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'metadata-edited.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not update metadata. Please check that date formats are correct.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+75
-62
@@ -3,71 +3,84 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
||||||
import blobStream from 'blob-stream';
|
import blobStream from 'blob-stream';
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
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 password = (
|
||||||
if (!password.trim()) {
|
document.getElementById('password-input') as HTMLInputElement
|
||||||
showAlert('Input Required', 'Please enter a password.');
|
).value;
|
||||||
return;
|
if (!password.trim()) {
|
||||||
|
showAlert('Input Required', 'Please enter a password.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoader('Preparing to process...');
|
||||||
|
const pdfData = await readFileAsArrayBuffer(file);
|
||||||
|
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer })
|
||||||
|
.promise;
|
||||||
|
const numPages = pdf.numPages;
|
||||||
|
const pageImages = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= numPages; i++) {
|
||||||
|
document.getElementById('loader-text').textContent =
|
||||||
|
`Processing page ${i} of ${numPages}...`;
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const viewport = page.getViewport({ scale: 2.0 });
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
await page.render({
|
||||||
|
canvasContext: context,
|
||||||
|
viewport: viewport,
|
||||||
|
canvas: canvas,
|
||||||
|
}).promise;
|
||||||
|
pageImages.push({
|
||||||
|
data: canvas.toDataURL('image/jpeg', 0.8),
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
document.getElementById('loader-text').textContent =
|
||||||
showLoader('Preparing to process...');
|
'Encrypting and building PDF...';
|
||||||
const pdfData = await readFileAsArrayBuffer(file);
|
const doc = new PDFDocument({
|
||||||
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer }).promise;
|
size: [pageImages[0].width, pageImages[0].height],
|
||||||
const numPages = pdf.numPages;
|
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
|
||||||
const pageImages = [];
|
userPassword: password,
|
||||||
|
ownerPassword: password,
|
||||||
for (let i = 1; i <= numPages; i++) {
|
permissions: {
|
||||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
printing: 'highResolution',
|
||||||
const page = await pdf.getPage(i);
|
modifying: false,
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
copying: false,
|
||||||
const canvas = document.createElement('canvas');
|
annotating: false,
|
||||||
const context = canvas.getContext('2d');
|
fillingForms: false,
|
||||||
canvas.height = viewport.height;
|
contentAccessibility: true,
|
||||||
canvas.width = viewport.width;
|
documentAssembly: false,
|
||||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
},
|
||||||
pageImages.push({
|
});
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
const stream = doc.pipe(blobStream());
|
||||||
width: viewport.width,
|
for (let i = 0; i < pageImages.length; i++) {
|
||||||
height: viewport.height
|
if (i > 0)
|
||||||
});
|
doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
||||||
}
|
doc.image(pageImages[i].data, 0, 0, {
|
||||||
|
width: pageImages[i].width,
|
||||||
document.getElementById('loader-text').textContent = 'Encrypting and building PDF...';
|
height: pageImages[i].height,
|
||||||
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 () {
|
|
||||||
const blob = stream.toBlob('application/pdf');
|
|
||||||
downloadFile(blob, `encrypted-${file.name}`);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Success', 'Encryption complete! Your download has started.');
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error during PDF encryption:", error);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
|
||||||
}
|
}
|
||||||
|
doc.end();
|
||||||
|
|
||||||
|
stream.on('finish', function () {
|
||||||
|
const blob = stream.toBlob('application/pdf');
|
||||||
|
downloadFile(blob, `encrypted-${file.name}`);
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Success', 'Encryption complete! Your download has started.');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during PDF encryption:', error);
|
||||||
|
hideLoader();
|
||||||
|
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,56 +6,65 @@ import JSZip from 'jszip';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function extractPages() {
|
export async function extractPages() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const pageInput = document.getElementById('pages-to-extract').value;
|
const pageInput = document.getElementById('pages-to-extract').value;
|
||||||
if (!pageInput.trim()) {
|
if (!pageInput.trim()) {
|
||||||
showAlert('Invalid Input', 'Please enter page numbers to extract.');
|
showAlert('Invalid Input', 'Please enter page numbers to extract.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
showLoader('Extracting pages...');
|
||||||
|
try {
|
||||||
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
|
const indicesToExtract = new Set();
|
||||||
|
const ranges = pageInput.split(',');
|
||||||
|
|
||||||
|
for (const range of ranges) {
|
||||||
|
const trimmedRange = range.trim();
|
||||||
|
if (trimmedRange.includes('-')) {
|
||||||
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
|
||||||
|
} else {
|
||||||
|
const pageNum = Number(trimmedRange);
|
||||||
|
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||||
|
indicesToExtract.add(pageNum - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
showLoader('Extracting pages...');
|
|
||||||
try {
|
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
|
||||||
const indicesToExtract = new Set();
|
|
||||||
const ranges = pageInput.split(',');
|
|
||||||
|
|
||||||
for (const range of ranges) {
|
if (indicesToExtract.size === 0) {
|
||||||
const trimmedRange = range.trim();
|
showAlert('Invalid Input', 'No valid pages selected for extraction.');
|
||||||
if (trimmedRange.includes('-')) {
|
hideLoader();
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
return;
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
|
||||||
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
|
|
||||||
} else {
|
|
||||||
const pageNum = Number(trimmedRange);
|
|
||||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
|
||||||
indicesToExtract.add(pageNum - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (indicesToExtract.size === 0) {
|
|
||||||
showAlert('Invalid Input', 'No valid pages selected for extraction.');
|
|
||||||
hideLoader();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const zip = new JSZip();
|
|
||||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
|
||||||
const sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
|
|
||||||
|
|
||||||
for (const index of sortedIndices) {
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
|
||||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
|
|
||||||
newPdf.addPage(copiedPage);
|
|
||||||
const newPdfBytes = await newPdf.save();
|
|
||||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
|
||||||
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
downloadFile(zipBlob, 'extracted-pages.zip');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Could not extract pages.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
|
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||||
|
const sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
for (const index of sortedIndices) {
|
||||||
|
const newPdf = await PDFLibDocument.create();
|
||||||
|
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
|
||||||
|
index as number,
|
||||||
|
]);
|
||||||
|
newPdf.addPage(copiedPage);
|
||||||
|
const newPdfBytes = await newPdf.save();
|
||||||
|
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||||
|
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'extracted-pages.zip');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not extract pages.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,84 +5,110 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||||
|
|
||||||
export function setupFixDimensionsUI() {
|
export function setupFixDimensionsUI() {
|
||||||
const targetSizeSelect = document.getElementById('target-size');
|
const targetSizeSelect = document.getElementById('target-size');
|
||||||
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);
|
|
||||||
|
|
||||||
showLoader('Standardizing pages...');
|
const scalingMode = (
|
||||||
try {
|
document.querySelector(
|
||||||
let targetWidth, targetHeight;
|
'input[name="scaling-mode"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
const backgroundColor = hexToRgb(
|
||||||
|
(document.getElementById('background-color') as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
|
||||||
if (targetSizeKey === 'Custom') {
|
showLoader('Standardizing pages...');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
try {
|
||||||
const width = parseFloat(document.getElementById('custom-width').value);
|
let targetWidth, targetHeight;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const height = parseFloat(document.getElementById('custom-height').value);
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const units = document.getElementById('custom-units').value;
|
|
||||||
if (units === 'in') {
|
|
||||||
targetWidth = width * 72;
|
|
||||||
targetHeight = height * 72;
|
|
||||||
} else { // mm
|
|
||||||
targetWidth = width * (72 / 25.4);
|
|
||||||
targetHeight = height * (72 / 25.4);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orientation === 'landscape' && targetWidth < targetHeight) {
|
if (targetSizeKey === 'Custom') {
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
|
const width = parseFloat(document.getElementById('custom-width').value);
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
}
|
const height = parseFloat(document.getElementById('custom-height').value);
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const sourceDoc = state.pdfDoc;
|
const units = document.getElementById('custom-units').value;
|
||||||
const newDoc = await PDFLibDocument.create();
|
if (units === 'in') {
|
||||||
|
targetWidth = width * 72;
|
||||||
for (const sourcePage of sourceDoc.getPages()) {
|
targetHeight = height * 72;
|
||||||
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
} else {
|
||||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
// mm
|
||||||
|
targetWidth = width * (72 / 25.4);
|
||||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
targetHeight = height * (72 / 25.4);
|
||||||
newPage.drawRectangle({ x: 0, y: 0, width: targetWidth, height: targetHeight, color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b) });
|
}
|
||||||
|
} else {
|
||||||
const scaleX = targetWidth / sourceWidth;
|
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
|
||||||
const scaleY = targetHeight / sourceHeight;
|
|
||||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
||||||
|
|
||||||
const scaledWidth = sourceWidth * scale;
|
|
||||||
const scaledHeight = sourceHeight * scale;
|
|
||||||
|
|
||||||
const x = (targetWidth - scaledWidth) / 2;
|
|
||||||
const y = (targetHeight - scaledHeight) / 2;
|
|
||||||
|
|
||||||
newPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'standardized.pdf');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'An error occurred while standardizing pages.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (orientation === 'landscape' && targetWidth < targetHeight) {
|
||||||
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||||
|
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
|
||||||
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceDoc = state.pdfDoc;
|
||||||
|
const newDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
|
for (const sourcePage of sourceDoc.getPages()) {
|
||||||
|
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
|
||||||
|
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||||
|
|
||||||
|
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||||
|
newPage.drawRectangle({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: targetWidth,
|
||||||
|
height: targetHeight,
|
||||||
|
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||||
|
});
|
||||||
|
|
||||||
|
const scaleX = targetWidth / sourceWidth;
|
||||||
|
const scaleY = targetHeight / sourceHeight;
|
||||||
|
const scale =
|
||||||
|
scalingMode === 'fit'
|
||||||
|
? Math.min(scaleX, scaleY)
|
||||||
|
: Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
|
const scaledWidth = sourceWidth * scale;
|
||||||
|
const scaledHeight = sourceHeight * scale;
|
||||||
|
|
||||||
|
const x = (targetWidth - scaledWidth) / 2;
|
||||||
|
const y = (targetHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
|
newPage.drawPage(embeddedPage, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: scaledWidth,
|
||||||
|
height: scaledHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdfBytes = await newDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'standardized.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'An error occurred while standardizing pages.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+25
-19
@@ -3,25 +3,31 @@ import { downloadFile } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
export async function flatten() {
|
export async function flatten() {
|
||||||
if (!state.pdfDoc) {
|
if (!state.pdfDoc) {
|
||||||
showAlert('Error', 'PDF not loaded.');
|
showAlert('Error', 'PDF not loaded.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Flattening PDF...');
|
showLoader('Flattening PDF...');
|
||||||
try {
|
try {
|
||||||
const form = state.pdfDoc.getForm();
|
const form = state.pdfDoc.getForm();
|
||||||
form.flatten();
|
form.flatten();
|
||||||
|
|
||||||
const flattenedBytes = await state.pdfDoc.save();
|
const flattenedBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([flattenedBytes], { type: 'application/pdf' }), 'flattened.pdf');
|
downloadFile(
|
||||||
} catch (e) {
|
new Blob([flattenedBytes], { type: 'application/pdf' }),
|
||||||
console.error(e);
|
'flattened.pdf'
|
||||||
if (e.message.includes('getForm')) {
|
);
|
||||||
showAlert('No Form Found', 'This PDF does not contain any form fields to flatten.');
|
} catch (e) {
|
||||||
} else {
|
console.error(e);
|
||||||
showAlert('Error', 'Could not flatten the PDF.');
|
if (e.message.includes('getForm')) {
|
||||||
}
|
showAlert(
|
||||||
} finally {
|
'No Form Found',
|
||||||
hideLoader();
|
'This PDF does not contain any form fields to flatten.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showAlert('Error', 'Could not flatten the PDF.');
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+334
-305
@@ -2,21 +2,21 @@ 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 {
|
import {
|
||||||
PDFDocument as PDFLibDocument,
|
PDFDocument as PDFLibDocument,
|
||||||
PDFTextField,
|
PDFTextField,
|
||||||
PDFCheckBox,
|
PDFCheckBox,
|
||||||
PDFRadioGroup,
|
PDFRadioGroup,
|
||||||
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';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
let pdfJsDoc: any = null;
|
let pdfJsDoc: any = null;
|
||||||
@@ -24,351 +24,380 @@ let currentPageNum = 1;
|
|||||||
let pdfRendering = false;
|
let pdfRendering = false;
|
||||||
let renderTimeout: any = null;
|
let renderTimeout: any = null;
|
||||||
const formState = {
|
const formState = {
|
||||||
scale: 2,
|
scale: 2,
|
||||||
fields: [],
|
fields: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
let fieldValues: Record<string, any> = {};
|
let fieldValues: Record<string, any> = {};
|
||||||
|
|
||||||
async function renderPage() {
|
async function renderPage() {
|
||||||
if (pdfRendering || !pdfJsDoc) return;
|
if (pdfRendering || !pdfJsDoc) return;
|
||||||
|
|
||||||
pdfRendering = true;
|
pdfRendering = true;
|
||||||
showLoader(`Rendering page ${currentPageNum}...`);
|
showLoader(`Rendering page ${currentPageNum}...`);
|
||||||
|
|
||||||
const page = await pdfJsDoc.getPage(currentPageNum);
|
const page = await pdfJsDoc.getPage(currentPageNum);
|
||||||
const viewport = page.getViewport({ scale: 1.0 });
|
const viewport = page.getViewport({ scale: 1.0 });
|
||||||
|
|
||||||
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
|
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
console.error('Could not get canvas context');
|
|
||||||
pdfRendering = false;
|
|
||||||
hideLoader();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
|
|
||||||
canvas.style.transformOrigin = 'top left';
|
|
||||||
canvas.style.transform = `scale(${formState.scale})`;
|
|
||||||
|
|
||||||
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), { ignoreEncryption: true });
|
|
||||||
const form = tempPdfDoc.getForm();
|
|
||||||
Object.keys(fieldValues).forEach(fieldName => {
|
|
||||||
try {
|
|
||||||
const field = form.getField(fieldName);
|
|
||||||
if (!field) return;
|
|
||||||
|
|
||||||
if (field instanceof PDFTextField) {
|
|
||||||
field.setText(fieldValues[fieldName]);
|
|
||||||
} else if (field instanceof PDFCheckBox) {
|
|
||||||
if (fieldValues[fieldName] === 'on') {
|
|
||||||
field.check();
|
|
||||||
} else {
|
|
||||||
field.uncheck();
|
|
||||||
}
|
|
||||||
} else if (field instanceof PDFRadioGroup) {
|
|
||||||
field.select(fieldValues[fieldName]);
|
|
||||||
} else if (field instanceof PDFDropdown) {
|
|
||||||
field.select(fieldValues[fieldName]);
|
|
||||||
} else if (field instanceof PDFOptionList) {
|
|
||||||
if (Array.isArray(fieldValues[fieldName])) {
|
|
||||||
(fieldValues[fieldName] as any[]).forEach(option => field.select(option));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error applying value to field "${fieldName}":`, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const tempPdfBytes = await tempPdfDoc.save();
|
|
||||||
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes }).promise;
|
|
||||||
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
|
||||||
|
|
||||||
await tempPage.render({
|
|
||||||
canvasContext: context,
|
|
||||||
viewport: viewport
|
|
||||||
} as any).promise;
|
|
||||||
|
|
||||||
const currentPageDisplay = document.getElementById('current-page-display');
|
|
||||||
const totalPagesDisplay = document.getElementById('total-pages-display');
|
|
||||||
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
|
||||||
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
|
|
||||||
|
|
||||||
if (currentPageDisplay) currentPageDisplay.textContent = String(currentPageNum);
|
|
||||||
if (totalPagesDisplay) totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
|
|
||||||
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
|
|
||||||
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
|
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
console.error('Could not get canvas context');
|
||||||
pdfRendering = false;
|
pdfRendering = false;
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
|
canvas.style.transformOrigin = 'top left';
|
||||||
|
canvas.style.transform = `scale(${formState.scale})`;
|
||||||
|
|
||||||
|
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), {
|
||||||
|
ignoreEncryption: true,
|
||||||
|
});
|
||||||
|
const form = tempPdfDoc.getForm();
|
||||||
|
Object.keys(fieldValues).forEach((fieldName) => {
|
||||||
|
try {
|
||||||
|
const field = form.getField(fieldName);
|
||||||
|
if (!field) return;
|
||||||
|
|
||||||
|
if (field instanceof PDFTextField) {
|
||||||
|
field.setText(fieldValues[fieldName]);
|
||||||
|
} else if (field instanceof PDFCheckBox) {
|
||||||
|
if (fieldValues[fieldName] === 'on') {
|
||||||
|
field.check();
|
||||||
|
} else {
|
||||||
|
field.uncheck();
|
||||||
|
}
|
||||||
|
} else if (field instanceof PDFRadioGroup) {
|
||||||
|
field.select(fieldValues[fieldName]);
|
||||||
|
} else if (field instanceof PDFDropdown) {
|
||||||
|
field.select(fieldValues[fieldName]);
|
||||||
|
} else if (field instanceof PDFOptionList) {
|
||||||
|
if (Array.isArray(fieldValues[fieldName])) {
|
||||||
|
(fieldValues[fieldName] as any[]).forEach((option) =>
|
||||||
|
field.select(option)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error applying value to field "${fieldName}":`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tempPdfBytes = await tempPdfDoc.save();
|
||||||
|
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes })
|
||||||
|
.promise;
|
||||||
|
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
||||||
|
|
||||||
|
await tempPage.render({
|
||||||
|
canvasContext: context,
|
||||||
|
viewport: viewport,
|
||||||
|
} as any).promise;
|
||||||
|
|
||||||
|
const currentPageDisplay = document.getElementById('current-page-display');
|
||||||
|
const totalPagesDisplay = document.getElementById('total-pages-display');
|
||||||
|
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
||||||
|
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
|
||||||
|
|
||||||
|
if (currentPageDisplay)
|
||||||
|
currentPageDisplay.textContent = String(currentPageNum);
|
||||||
|
if (totalPagesDisplay)
|
||||||
|
totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
|
||||||
|
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
|
||||||
|
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
|
||||||
|
|
||||||
|
pdfRendering = false;
|
||||||
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changePage(offset: number) {
|
async function changePage(offset: number) {
|
||||||
const newPageNum = currentPageNum + offset;
|
const newPageNum = currentPageNum + offset;
|
||||||
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
|
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
|
||||||
currentPageNum = newPageNum;
|
currentPageNum = newPageNum;
|
||||||
await renderPage();
|
await renderPage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setZoom(factor: number) {
|
async function setZoom(factor: number) {
|
||||||
formState.scale = factor;
|
formState.scale = factor;
|
||||||
await renderPage();
|
await renderPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFormChange(event: Event) {
|
function handleFormChange(event: Event) {
|
||||||
const input = event.target as HTMLInputElement | HTMLSelectElement;
|
const input = event.target as HTMLInputElement | HTMLSelectElement;
|
||||||
const name = input.name;
|
const name = input.name;
|
||||||
let value: any;
|
let value: any;
|
||||||
|
|
||||||
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
|
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldValues[name] = value;
|
fieldValues[name] = value;
|
||||||
|
|
||||||
clearTimeout(renderTimeout);
|
clearTimeout(renderTimeout);
|
||||||
renderTimeout = setTimeout(() => {
|
renderTimeout = setTimeout(() => {
|
||||||
renderPage();
|
renderPage();
|
||||||
}, 350);
|
}, 350);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createFormFieldHtml(field: any): HTMLElement {
|
function createFormFieldHtml(field: any): HTMLElement {
|
||||||
const name = field.getName();
|
const name = field.getName();
|
||||||
const isRequired = field.isRequired();
|
const isRequired = field.isRequired();
|
||||||
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}`;
|
||||||
label.className = 'block text-sm font-medium text-gray-300 capitalize mb-1';
|
label.className = 'block text-sm font-medium text-gray-300 capitalize mb-1';
|
||||||
label.textContent = labelText;
|
label.textContent = labelText;
|
||||||
|
|
||||||
if (isRequired) {
|
if (isRequired) {
|
||||||
const requiredSpan = document.createElement('span');
|
const requiredSpan = document.createElement('span');
|
||||||
requiredSpan.className = 'text-red-500';
|
requiredSpan.className = 'text-red-500';
|
||||||
requiredSpan.textContent = ' *';
|
requiredSpan.textContent = ' *';
|
||||||
label.appendChild(requiredSpan);
|
label.appendChild(requiredSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.appendChild(label);
|
||||||
|
|
||||||
|
let inputElement: HTMLElement | DocumentFragment;
|
||||||
|
|
||||||
|
if (field instanceof PDFTextField) {
|
||||||
|
fieldValues[name] = field.getText() || '';
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.id = `field-${name}`;
|
||||||
|
input.name = name;
|
||||||
|
input.value = fieldValues[name];
|
||||||
|
input.className =
|
||||||
|
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||||
|
inputElement = input;
|
||||||
|
} else if (field instanceof PDFCheckBox) {
|
||||||
|
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.id = `field-${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.checked = field.isChecked();
|
||||||
|
inputElement = input;
|
||||||
|
} else if (field instanceof PDFRadioGroup) {
|
||||||
|
fieldValues[name] = field.getSelected();
|
||||||
|
const options = field.getOptions();
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
options.forEach((opt: string) => {
|
||||||
|
const optionLabel = document.createElement('label');
|
||||||
|
optionLabel.className = 'flex items-center gap-2';
|
||||||
|
|
||||||
|
const radio = document.createElement('input');
|
||||||
|
radio.type = 'radio';
|
||||||
|
radio.name = name;
|
||||||
|
radio.value = opt;
|
||||||
|
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;
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'text-gray-300 text-sm';
|
||||||
|
span.textContent = opt;
|
||||||
|
|
||||||
|
optionLabel.append(radio, span);
|
||||||
|
fragment.appendChild(optionLabel);
|
||||||
|
});
|
||||||
|
inputElement = fragment;
|
||||||
|
} else if (field instanceof PDFDropdown || field instanceof PDFOptionList) {
|
||||||
|
const selectedValues = field.getSelected();
|
||||||
|
fieldValues[name] = selectedValues;
|
||||||
|
const options = field.getOptions();
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.id = `field-${name}`;
|
||||||
|
select.name = name;
|
||||||
|
select.className =
|
||||||
|
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||||
|
|
||||||
|
if (field instanceof PDFOptionList) {
|
||||||
|
select.multiple = true;
|
||||||
|
select.size = Math.min(10, options.length);
|
||||||
|
select.classList.add('h-auto');
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper.appendChild(label);
|
options.forEach((opt: string) => {
|
||||||
|
const option = document.createElement('option');
|
||||||
let inputElement: HTMLElement | DocumentFragment;
|
option.value = opt;
|
||||||
|
option.textContent = opt;
|
||||||
if (field instanceof PDFTextField) {
|
if (selectedValues.includes(opt)) option.selected = true;
|
||||||
fieldValues[name] = field.getText() || '';
|
select.appendChild(option);
|
||||||
const input = document.createElement('input');
|
});
|
||||||
input.type = 'text';
|
inputElement = select;
|
||||||
input.id = `field-${name}`;
|
} else {
|
||||||
input.name = name;
|
const unsupportedDiv = document.createElement('div');
|
||||||
input.value = fieldValues[name];
|
unsupportedDiv.className =
|
||||||
input.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
inputElement = input;
|
const p = document.createElement('p');
|
||||||
} else if (field instanceof PDFCheckBox) {
|
p.className = 'text-sm text-gray-400';
|
||||||
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
if (field instanceof PDFSignature) {
|
||||||
const input = document.createElement('input');
|
p.textContent = 'Signature field: Not supported for direct editing.';
|
||||||
input.type = 'checkbox';
|
} else if (field instanceof PDFButton) {
|
||||||
input.id = `field-${name}`;
|
p.textContent = `Button: ${labelText}`;
|
||||||
input.name = name;
|
|
||||||
input.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
|
||||||
input.checked = field.isChecked();
|
|
||||||
inputElement = input;
|
|
||||||
} else if (field instanceof PDFRadioGroup) {
|
|
||||||
fieldValues[name] = field.getSelected();
|
|
||||||
const options = field.getOptions();
|
|
||||||
const fragment = document.createDocumentFragment();
|
|
||||||
options.forEach((opt: string) => {
|
|
||||||
const optionLabel = document.createElement('label');
|
|
||||||
optionLabel.className = 'flex items-center gap-2';
|
|
||||||
|
|
||||||
const radio = document.createElement('input');
|
|
||||||
radio.type = 'radio';
|
|
||||||
radio.name = name;
|
|
||||||
radio.value = opt;
|
|
||||||
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;
|
|
||||||
|
|
||||||
const span = document.createElement('span');
|
|
||||||
span.className = 'text-gray-300 text-sm';
|
|
||||||
span.textContent = opt;
|
|
||||||
|
|
||||||
optionLabel.append(radio, span);
|
|
||||||
fragment.appendChild(optionLabel);
|
|
||||||
});
|
|
||||||
inputElement = fragment;
|
|
||||||
} else if (field instanceof PDFDropdown || field instanceof PDFOptionList) {
|
|
||||||
const selectedValues = field.getSelected();
|
|
||||||
fieldValues[name] = selectedValues;
|
|
||||||
const options = field.getOptions();
|
|
||||||
|
|
||||||
const select = document.createElement('select');
|
|
||||||
select.id = `field-${name}`;
|
|
||||||
select.name = name;
|
|
||||||
select.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
|
||||||
|
|
||||||
if (field instanceof PDFOptionList) {
|
|
||||||
select.multiple = true;
|
|
||||||
select.size = Math.min(10, options.length);
|
|
||||||
select.classList.add('h-auto');
|
|
||||||
}
|
|
||||||
|
|
||||||
options.forEach((opt: string) => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = opt;
|
|
||||||
option.textContent = opt;
|
|
||||||
if (selectedValues.includes(opt)) option.selected = true;
|
|
||||||
select.appendChild(option);
|
|
||||||
});
|
|
||||||
inputElement = select;
|
|
||||||
} else {
|
} else {
|
||||||
const unsupportedDiv = document.createElement('div');
|
p.textContent = `Unsupported field type: ${field.constructor.name}`;
|
||||||
unsupportedDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
|
||||||
const p = document.createElement('p');
|
|
||||||
p.className = 'text-sm text-gray-400';
|
|
||||||
if (field instanceof PDFSignature) {
|
|
||||||
p.textContent = 'Signature field: Not supported for direct editing.';
|
|
||||||
} else if (field instanceof PDFButton) {
|
|
||||||
p.textContent = `Button: ${labelText}`;
|
|
||||||
} else {
|
|
||||||
p.textContent = `Unsupported field type: ${field.constructor.name}`;
|
|
||||||
}
|
|
||||||
unsupportedDiv.appendChild(p);
|
|
||||||
return unsupportedDiv;
|
|
||||||
}
|
}
|
||||||
|
unsupportedDiv.appendChild(p);
|
||||||
|
return unsupportedDiv;
|
||||||
|
}
|
||||||
|
|
||||||
wrapper.appendChild(inputElement);
|
wrapper.appendChild(inputElement);
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupFormFiller() {
|
export async function setupFormFiller() {
|
||||||
if (!state.pdfDoc) return;
|
if (!state.pdfDoc) return;
|
||||||
|
|
||||||
showLoader('Analyzing form fields...');
|
showLoader('Analyzing form fields...');
|
||||||
const formContainer = document.getElementById('form-fields-container');
|
const formContainer = document.getElementById('form-fields-container');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
if (!formContainer || !processBtn) {
|
if (!formContainer || !processBtn) {
|
||||||
console.error('Required DOM elements not found');
|
console.error('Required DOM elements not found');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const form = state.pdfDoc.getForm();
|
const form = state.pdfDoc.getForm();
|
||||||
const fields = form.getFields();
|
const fields = form.getFields();
|
||||||
formState.fields = fields;
|
formState.fields = fields;
|
||||||
|
|
||||||
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 =
|
||||||
processBtn.classList.add('hidden');
|
'<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
||||||
} else {
|
processBtn.classList.add('hidden');
|
||||||
fields.forEach((field: any) => {
|
} else {
|
||||||
try {
|
fields.forEach((field: any) => {
|
||||||
const fieldElement = createFormFieldHtml(field);
|
try {
|
||||||
formContainer.appendChild(fieldElement);
|
const fieldElement = createFormFieldHtml(field);
|
||||||
} catch (e: any) {
|
formContainer.appendChild(fieldElement);
|
||||||
console.error(`Error processing field "${field.getName()}":`, e);
|
} catch (e: any) {
|
||||||
const errorDiv = document.createElement('div');
|
console.error(`Error processing field "${field.getName()}":`, e);
|
||||||
errorDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
const errorDiv = document.createElement('div');
|
||||||
// Sanitize error message display
|
errorDiv.className =
|
||||||
const p1 = document.createElement('p');
|
'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
p1.className = 'text-sm text-gray-500';
|
// Sanitize error message display
|
||||||
p1.textContent = `Unsupported field: ${field.getName()}`;
|
const p1 = document.createElement('p');
|
||||||
const p2 = document.createElement('p');
|
p1.className = 'text-sm text-gray-500';
|
||||||
p2.className = 'text-xs text-gray-500';
|
p1.textContent = `Unsupported field: ${field.getName()}`;
|
||||||
p2.textContent = e.message;
|
const p2 = document.createElement('p');
|
||||||
errorDiv.append(p1, p2);
|
p2.className = 'text-xs text-gray-500';
|
||||||
formContainer.appendChild(errorDiv);
|
p2.textContent = e.message;
|
||||||
}
|
errorDiv.append(p1, p2);
|
||||||
});
|
formContainer.appendChild(errorDiv);
|
||||||
|
|
||||||
processBtn.classList.remove('hidden');
|
|
||||||
formContainer.addEventListener('change', handleFormChange);
|
|
||||||
formContainer.addEventListener('input', handleFormChange);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
processBtn.classList.remove('hidden');
|
||||||
pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
formContainer.addEventListener('change', handleFormChange);
|
||||||
currentPageNum = 1;
|
formContainer.addEventListener('input', handleFormChange);
|
||||||
await renderPage();
|
|
||||||
|
|
||||||
const zoomInBtn = document.getElementById('zoom-in-btn');
|
|
||||||
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
|
||||||
const prevPageBtn = document.getElementById('prev-page');
|
|
||||||
const nextPageBtn = document.getElementById('next-page');
|
|
||||||
|
|
||||||
if (zoomInBtn) zoomInBtn.addEventListener('click', () => 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));
|
|
||||||
|
|
||||||
hideLoader();
|
|
||||||
|
|
||||||
const formFillerOptions = document.getElementById('form-filler-options');
|
|
||||||
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Critical error setting up form filler:", e);
|
|
||||||
showAlert('Error', 'Failed to read PDF form data. The file may be corrupt or not a valid form.');
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
|
pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||||
|
currentPageNum = 1;
|
||||||
|
await renderPage();
|
||||||
|
|
||||||
|
const zoomInBtn = document.getElementById('zoom-in-btn');
|
||||||
|
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
||||||
|
const prevPageBtn = document.getElementById('prev-page');
|
||||||
|
const nextPageBtn = document.getElementById('next-page');
|
||||||
|
|
||||||
|
if (zoomInBtn)
|
||||||
|
zoomInBtn.addEventListener('click', () =>
|
||||||
|
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));
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
const formFillerOptions = document.getElementById('form-filler-options');
|
||||||
|
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Critical error setting up form filler:', e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to read PDF form data. The file may be corrupt or not a valid form.'
|
||||||
|
);
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processAndDownloadForm() {
|
export async function processAndDownloadForm() {
|
||||||
showLoader('Applying form data...');
|
showLoader('Applying form data...');
|
||||||
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];
|
||||||
|
|
||||||
if (field instanceof PDFTextField) {
|
if (field instanceof PDFTextField) {
|
||||||
field.setText(value);
|
field.setText(value);
|
||||||
} else if (field instanceof PDFCheckBox) {
|
} else if (field instanceof PDFCheckBox) {
|
||||||
if (value === 'on') {
|
if (value === 'on') {
|
||||||
field.check();
|
field.check();
|
||||||
} else {
|
} else {
|
||||||
field.uncheck();
|
field.uncheck();
|
||||||
}
|
}
|
||||||
} else if (field instanceof PDFRadioGroup) {
|
} else if (field instanceof PDFRadioGroup) {
|
||||||
field.select(value);
|
field.select(value);
|
||||||
} else if (field instanceof PDFDropdown) {
|
} else if (field instanceof PDFDropdown) {
|
||||||
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.');
|
} finally {
|
||||||
} finally {
|
hideLoader();
|
||||||
hideLoader();
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+38
-26
@@ -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';
|
||||||
@@ -6,31 +5,44 @@ import heic2any from 'heic2any';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function heicToPdf() {
|
export async function heicToPdf() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one HEIC file.');
|
showAlert('No Files', 'Please select at least one HEIC file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Converting HEIC to PDF...');
|
showLoader('Converting HEIC to PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfDoc = await PDFLibDocument.create();
|
const pdfDoc = await PDFLibDocument.create();
|
||||||
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)
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
? conversionResult[0]
|
||||||
|
: conversionResult;
|
||||||
|
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,
|
||||||
const pdfBytes = await pdfDoc.save();
|
y: 0,
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_heic.pdf');
|
width: pngImage.width,
|
||||||
} catch (e) {
|
height: pngImage.height,
|
||||||
console.error(e);
|
});
|
||||||
showAlert('Error', 'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_heic.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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,62 +74,77 @@ 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.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Converting images to PDF...');
|
showLoader('Converting images to PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfDoc = await PDFLibDocument.create();
|
const pdfDoc = await PDFLibDocument.create();
|
||||||
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) {
|
||||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
let image;
|
let image;
|
||||||
|
|
||||||
if (file.type === 'image/jpeg') {
|
if (file.type === 'image/jpeg') {
|
||||||
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(
|
||||||
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
|
`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`
|
||||||
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
);
|
||||||
}
|
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
|
||||||
} else if (file.type === 'image/png') {
|
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||||
try {
|
|
||||||
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`);
|
|
||||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
|
||||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For WebP and other types, convert to PNG to preserve transparency
|
|
||||||
console.warn(`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`);
|
|
||||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
|
||||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = pdfDoc.addPage([image.width, image.height]);
|
|
||||||
page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
|
||||||
}
|
}
|
||||||
|
} else if (file.type === 'image/png') {
|
||||||
if (pdfDoc.getPageCount() === 0) {
|
try {
|
||||||
throw new Error("No valid images could be processed. Please check your files.");
|
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`
|
||||||
|
);
|
||||||
|
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||||
|
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// For WebP and other types, convert to PNG to preserve transparency
|
||||||
|
console.warn(
|
||||||
|
`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`
|
||||||
|
);
|
||||||
|
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||||
|
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||||
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const page = pdfDoc.addPage([image.width, image.height]);
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from-images.pdf');
|
page.drawImage(image, {
|
||||||
} catch (e) {
|
x: 0,
|
||||||
console.error(e);
|
y: 0,
|
||||||
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
width: image.width,
|
||||||
} finally {
|
height: image.height,
|
||||||
hideLoader();
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pdfDoc.getPageCount() === 0) {
|
||||||
|
throw new Error(
|
||||||
|
'No valid images could be processed. Please check your files.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from-images.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+82
-56
@@ -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,64 +49,90 @@ 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';
|
||||||
|
|
||||||
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,
|
||||||
organize,
|
organize,
|
||||||
rotate,
|
rotate,
|
||||||
'add-page-numbers': addPageNumbers,
|
'add-page-numbers': addPageNumbers,
|
||||||
'pdf-to-jpg': pdfToJpg,
|
'pdf-to-jpg': pdfToJpg,
|
||||||
'jpg-to-pdf': jpgToPdf,
|
'jpg-to-pdf': jpgToPdf,
|
||||||
'scan-to-pdf': scanToPdf,
|
'scan-to-pdf': scanToPdf,
|
||||||
compress,
|
compress,
|
||||||
'pdf-to-greyscale': pdfToGreyscale,
|
'pdf-to-greyscale': pdfToGreyscale,
|
||||||
'pdf-to-zip': pdfToZip,
|
'pdf-to-zip': pdfToZip,
|
||||||
'edit-metadata': editMetadata,
|
'edit-metadata': editMetadata,
|
||||||
'remove-metadata': removeMetadata,
|
'remove-metadata': removeMetadata,
|
||||||
flatten,
|
flatten,
|
||||||
'pdf-to-png': pdfToPng,
|
'pdf-to-png': pdfToPng,
|
||||||
'png-to-pdf': pngToPdf,
|
'png-to-pdf': pngToPdf,
|
||||||
'pdf-to-webp': pdfToWebp,
|
'pdf-to-webp': pdfToWebp,
|
||||||
'webp-to-pdf': webpToPdf,
|
'webp-to-pdf': webpToPdf,
|
||||||
'delete-pages': deletePages,
|
'delete-pages': deletePages,
|
||||||
'add-blank-page': addBlankPage,
|
'add-blank-page': addBlankPage,
|
||||||
'extract-pages': extractPages,
|
'extract-pages': extractPages,
|
||||||
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
|
||||||
'add-header-footer': addHeaderFooter,
|
'add-header-footer': addHeaderFooter,
|
||||||
'image-to-pdf': imageToPdf,
|
'image-to-pdf': imageToPdf,
|
||||||
'change-permissions': changePermissions,
|
'change-permissions': changePermissions,
|
||||||
'pdf-to-markdown': pdfToMarkdown,
|
'pdf-to-markdown': pdfToMarkdown,
|
||||||
'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,
|
||||||
'tiff-to-pdf': tiffToPdf,
|
'tiff-to-pdf': tiffToPdf,
|
||||||
'pdf-to-bmp': pdfToBmp,
|
'pdf-to-bmp': pdfToBmp,
|
||||||
'pdf-to-tiff': pdfToTiff,
|
'pdf-to-tiff': pdfToTiff,
|
||||||
'split-in-half': splitInHalf,
|
'split-in-half': splitInHalf,
|
||||||
'page-dimensions': analyzeAndDisplayDimensions,
|
'page-dimensions': analyzeAndDisplayDimensions,
|
||||||
'n-up': { process: nUpTool, setup: setupNUpUI },
|
'n-up': { process: nUpTool, setup: setupNUpUI },
|
||||||
'duplicate-organize': { process: processAndSave },
|
'duplicate-organize': { process: processAndSave },
|
||||||
'combine-single-page': combineToSinglePage,
|
'combine-single-page': combineToSinglePage,
|
||||||
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
|
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
|
||||||
'change-background-color': changeBackgroundColor,
|
'change-background-color': changeBackgroundColor,
|
||||||
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
|
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
|
||||||
'compare-pdfs': { setup: setupCompareTool },
|
'compare-pdfs': { setup: setupCompareTool },
|
||||||
'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,
|
||||||
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller},
|
setup: setupRemoveAnnotationsTool,
|
||||||
'posterize': { process: posterize, setup: setupPosterizeTool },
|
},
|
||||||
|
cropper: { setup: setupCropperTool },
|
||||||
|
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller },
|
||||||
|
posterize: { process: posterize, setup: setupPosterizeTool },
|
||||||
|
'remove-blank-pages': {
|
||||||
|
process: removeBlankPages,
|
||||||
|
setup: setupRemoveBlankPagesTool,
|
||||||
|
},
|
||||||
|
'alternate-merge': {
|
||||||
|
process: alternateMerge,
|
||||||
|
setup: setupAlternateMergeTool,
|
||||||
|
},
|
||||||
|
linearize: linearizePdf,
|
||||||
|
'add-attachments': {
|
||||||
|
process: addAttachments,
|
||||||
|
setup: setupAddAttachmentsTool,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@@ -4,49 +4,62 @@ 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) {
|
||||||
showLoader('Inverting PDF colors...');
|
showAlert('Error', 'PDF not loaded.');
|
||||||
try {
|
return;
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
}
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
showLoader('Inverting PDF colors...');
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
try {
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
|
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||||
const page = await pdfjsDoc.getPage(i);
|
const page = await pdfjsDoc.getPage(i);
|
||||||
const viewport = page.getViewport({ scale: 1.5 });
|
const viewport = page.getViewport({ scale: 1.5 });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||||
|
|
||||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
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) {
|
||||||
data[j] = 255 - data[j]; // red
|
data[j] = 255 - data[j]; // red
|
||||||
data[j + 1] = 255 - data[j + 1]; // green
|
data[j + 1] = 255 - data[j + 1]; // green
|
||||||
data[j + 2] = 255 - data[j + 2]; // blue
|
data[j + 2] = 255 - data[j + 2]; // blue
|
||||||
}
|
}
|
||||||
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) =>
|
||||||
const reader = new FileReader();
|
canvas.toBlob((blob) => {
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
const reader = new FileReader();
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
}, 'image/png'));
|
reader.readAsArrayBuffer(blob);
|
||||||
|
}, '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,
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
y: 0,
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'inverted.pdf');
|
width: image.width,
|
||||||
} catch (e) {
|
height: image.height,
|
||||||
console.error(e);
|
});
|
||||||
showAlert('Error', 'Could not invert PDF colors.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'inverted.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not invert PDF colors.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+343
-314
@@ -7,390 +7,419 @@ import * as pdfjsLib from 'pdfjs-dist';
|
|||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
const mergeState = {
|
const mergeState = {
|
||||||
pdfDocs: {},
|
pdfDocs: {},
|
||||||
activeMode: 'file',
|
activeMode: 'file',
|
||||||
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) {
|
||||||
const indices = new Set();
|
const indices = new Set();
|
||||||
if (!rangeString.trim()) return [];
|
if (!rangeString.trim()) return [];
|
||||||
|
|
||||||
const ranges = rangeString.split(',');
|
const ranges = rangeString.split(',');
|
||||||
for (const range of ranges) {
|
for (const range of ranges) {
|
||||||
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 (
|
||||||
for (let i = start; i <= end; i++) {
|
isNaN(start) ||
|
||||||
indices.add(i - 1);
|
isNaN(end) ||
|
||||||
}
|
start < 1 ||
|
||||||
} else {
|
end > totalPages ||
|
||||||
const pageNum = Number(trimmedRange);
|
start > end
|
||||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
)
|
||||||
indices.add(pageNum - 1);
|
continue;
|
||||||
}
|
for (let i = start; i <= end; i++) {
|
||||||
|
indices.add(i - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pageNum = Number(trimmedRange);
|
||||||
|
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||||
|
indices.add(pageNum - 1);
|
||||||
}
|
}
|
||||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
}
|
||||||
return Array.from(indices).sort((a, b) => a - b);
|
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||||
|
return Array.from(indices).sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeFileListSortable() {
|
function initializeFileListSortable() {
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
if (!fileList) return;
|
if (!fileList) return;
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||||
|
if (mergeState.sortableInstances.fileList) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||||
if (mergeState.sortableInstances.fileList) {
|
mergeState.sortableInstances.fileList.destroy();
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
}
|
||||||
mergeState.sortableInstances.fileList.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
|
||||||
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
|
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
|
||||||
handle: '.drag-handle',
|
handle: '.drag-handle',
|
||||||
animation: 150,
|
animation: 150,
|
||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
chosenClass: 'sortable-chosen',
|
chosenClass: 'sortable-chosen',
|
||||||
dragClass: 'sortable-drag',
|
dragClass: 'sortable-drag',
|
||||||
onStart: function (evt: any) {
|
onStart: function (evt: any) {
|
||||||
evt.item.style.opacity = '0.5';
|
evt.item.style.opacity = '0.5';
|
||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: any) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializePageThumbnailsSortable() {
|
function initializePageThumbnailsSortable() {
|
||||||
const container = document.getElementById('page-merge-preview');
|
const container = document.getElementById('page-merge-preview');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||||
|
if (mergeState.sortableInstances.pageThumbnails) {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||||
if (mergeState.sortableInstances.pageThumbnails) {
|
mergeState.sortableInstances.pageThumbnails.destroy();
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
}
|
||||||
mergeState.sortableInstances.pageThumbnails.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'pageThumbnails' does not exist on type '... Remove this comment to see the full error message
|
||||||
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
|
mergeState.sortableInstances.pageThumbnails = Sortable.create(container, {
|
||||||
animation: 150,
|
animation: 150,
|
||||||
ghostClass: 'sortable-ghost',
|
ghostClass: 'sortable-ghost',
|
||||||
chosenClass: 'sortable-chosen',
|
chosenClass: 'sortable-chosen',
|
||||||
dragClass: 'sortable-drag',
|
dragClass: 'sortable-drag',
|
||||||
onStart: function (evt: any) {
|
onStart: function (evt: any) {
|
||||||
evt.item.style.opacity = '0.5';
|
evt.item.style.opacity = '0.5';
|
||||||
},
|
},
|
||||||
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() {
|
||||||
const container = document.getElementById('page-merge-preview');
|
const container = document.getElementById('page-merge-preview');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const currentFileHash = generateFileHash();
|
const currentFileHash = generateFileHash();
|
||||||
const filesChanged = currentFileHash !== mergeState.lastFileHash;
|
const filesChanged = currentFileHash !== mergeState.lastFileHash;
|
||||||
|
|
||||||
if (!filesChanged && mergeState.cachedThumbnails !== null) {
|
if (!filesChanged && mergeState.cachedThumbnails !== null) {
|
||||||
// Simple check to see if it's already rendered to avoid flicker.
|
// Simple check to see if it's already rendered to avoid flicker.
|
||||||
if (container.firstChild) {
|
if (container.firstChild) {
|
||||||
initializePageThumbnailsSortable();
|
initializePageThumbnailsSortable();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mergeState.isRendering) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeState.isRendering = true;
|
||||||
|
container.textContent = '';
|
||||||
|
|
||||||
|
let currentPageNumber = 0;
|
||||||
|
let totalPages = state.files.reduce((sum, file) => {
|
||||||
|
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||||
|
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const thumbnailsHTML = [];
|
||||||
|
|
||||||
|
for (const file of state.files) {
|
||||||
|
const pdfDoc = mergeState.pdfDocs[file.name];
|
||||||
|
if (!pdfDoc) continue;
|
||||||
|
|
||||||
|
const pdfData = await pdfDoc.save();
|
||||||
|
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||||
|
currentPageNumber++;
|
||||||
|
showLoader(
|
||||||
|
`Rendering page previews: ${currentPageNumber}/${totalPages}`
|
||||||
|
);
|
||||||
|
const page = await pdfjsDoc.getPage(i);
|
||||||
|
const viewport = page.getViewport({ scale: 0.3 });
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
const context = canvas.getContext('2d')!;
|
||||||
|
await page.render({
|
||||||
|
canvasContext: context,
|
||||||
|
canvas: canvas,
|
||||||
|
viewport,
|
||||||
|
}).promise;
|
||||||
|
|
||||||
|
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.dataset.fileName = file.name;
|
||||||
|
wrapper.dataset.pageIndex = (i - 1).toString();
|
||||||
|
|
||||||
|
const imgContainer = document.createElement('div');
|
||||||
|
imgContainer.className = 'relative';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = canvas.toDataURL();
|
||||||
|
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
||||||
|
|
||||||
|
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.textContent = i.toString();
|
||||||
|
|
||||||
|
imgContainer.append(img, pageNumDiv);
|
||||||
|
|
||||||
|
const fileNamePara = document.createElement('p');
|
||||||
|
fileNamePara.className =
|
||||||
|
'text-xs text-gray-400 truncate w-full text-center';
|
||||||
|
const fullTitle = `${file.name} (page ${i})`;
|
||||||
|
fileNamePara.title = fullTitle;
|
||||||
|
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
||||||
|
|
||||||
|
wrapper.append(imgContainer, fileNamePara);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfjsDoc.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mergeState.isRendering) {
|
mergeState.cachedThumbnails = true;
|
||||||
return;
|
mergeState.lastFileHash = currentFileHash;
|
||||||
}
|
|
||||||
|
|
||||||
mergeState.isRendering = true;
|
initializePageThumbnailsSortable();
|
||||||
container.textContent = '';
|
} catch (error) {
|
||||||
|
console.error('Error rendering page thumbnails:', error);
|
||||||
let currentPageNumber = 0;
|
showAlert('Error', 'Failed to render page thumbnails');
|
||||||
let totalPages = state.files.reduce((sum, file) => {
|
} finally {
|
||||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
hideLoader();
|
||||||
return sum + (pdfDoc ? pdfDoc.getPageCount() : 0);
|
mergeState.isRendering = false;
|
||||||
}, 0);
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const thumbnailsHTML = [];
|
|
||||||
|
|
||||||
for (const file of state.files) {
|
|
||||||
const pdfDoc = mergeState.pdfDocs[file.name];
|
|
||||||
if (!pdfDoc) continue;
|
|
||||||
|
|
||||||
const pdfData = await pdfDoc.save();
|
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
|
||||||
currentPageNumber++;
|
|
||||||
showLoader(`Rendering page previews: ${currentPageNumber}/${totalPages}`);
|
|
||||||
const page = await pdfjsDoc.getPage(i);
|
|
||||||
const viewport = page.getViewport({ scale: 0.3 });
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
const context = canvas.getContext('2d')!;
|
|
||||||
await page.render({
|
|
||||||
canvasContext: context,
|
|
||||||
canvas: canvas,
|
|
||||||
viewport
|
|
||||||
}).promise;
|
|
||||||
|
|
||||||
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.dataset.fileName = file.name;
|
|
||||||
wrapper.dataset.pageIndex = (i - 1).toString();
|
|
||||||
|
|
||||||
const imgContainer = document.createElement('div');
|
|
||||||
imgContainer.className = 'relative';
|
|
||||||
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.src = canvas.toDataURL();
|
|
||||||
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
|
||||||
|
|
||||||
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.textContent = i.toString();
|
|
||||||
|
|
||||||
imgContainer.append(img, pageNumDiv);
|
|
||||||
|
|
||||||
const fileNamePara = document.createElement('p');
|
|
||||||
fileNamePara.className = 'text-xs text-gray-400 truncate w-full text-center';
|
|
||||||
const fullTitle = `${file.name} (page ${i})`;
|
|
||||||
fileNamePara.title = fullTitle;
|
|
||||||
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
|
||||||
|
|
||||||
wrapper.append(imgContainer, fileNamePara);
|
|
||||||
container.appendChild(wrapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
pdfjsDoc.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeState.cachedThumbnails = true;
|
|
||||||
mergeState.lastFileHash = currentFileHash;
|
|
||||||
|
|
||||||
initializePageThumbnailsSortable();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error rendering page thumbnails:', error);
|
|
||||||
showAlert('Error', 'Failed to render page thumbnails');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
mergeState.isRendering = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function merge() {
|
export async function merge() {
|
||||||
showLoader('Merging PDFs...');
|
showLoader('Merging PDFs...');
|
||||||
try {
|
try {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
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)
|
||||||
// @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) => {
|
||||||
return state.files.find(f => f.name === li.dataset.fileName);
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
}).filter(Boolean);
|
return state.files.find((f) => f.name === li.dataset.fileName);
|
||||||
|
})
|
||||||
|
.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, '_');
|
||||||
const rangeInput = document.getElementById(`range-${safeFileName}`);
|
const rangeInput = document.getElementById(`range-${safeFileName}`);
|
||||||
if (!rangeInput) continue;
|
if (!rangeInput) continue;
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const rangeInputValue = rangeInput.value;
|
const rangeInputValue = rangeInput.value;
|
||||||
const sourcePdf = mergeState.pdfDocs[file.name];
|
const sourcePdf = mergeState.pdfDocs[file.name];
|
||||||
if (!sourcePdf) continue;
|
if (!sourcePdf) continue;
|
||||||
|
|
||||||
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 =
|
||||||
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
||||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
||||||
}
|
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pageContainer = document.getElementById('page-merge-preview');
|
||||||
|
const pageElements = Array.from(pageContainer.children);
|
||||||
|
|
||||||
} else {
|
for (const el of pageElements) {
|
||||||
const pageContainer = document.getElementById('page-merge-preview');
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
const pageElements = Array.from(pageContainer.children);
|
const fileName = el.dataset.fileName;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
|
const pageIndex = parseInt(el.dataset.pageIndex, 10);
|
||||||
|
|
||||||
for (const el of pageElements) {
|
const sourcePdf = mergeState.pdfDocs[fileName];
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
if (sourcePdf && !isNaN(pageIndex)) {
|
||||||
const fileName = el.dataset.fileName;
|
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
pageIndex,
|
||||||
const pageIndex = parseInt(el.dataset.pageIndex, 10);
|
]);
|
||||||
|
newPdfDoc.addPage(copiedPage);
|
||||||
const sourcePdf = mergeState.pdfDocs[fileName];
|
|
||||||
if (sourcePdf && !isNaN(pageIndex)) {
|
|
||||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [pageIndex]);
|
|
||||||
newPdfDoc.addPage(copiedPage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const mergedPdfBytes = await newPdfDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), 'merged.pdf');
|
|
||||||
showAlert('Success', 'PDFs merged successfully!');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Merge error:', e);
|
|
||||||
showAlert('Error', 'Failed to merge PDFs. Please check that all files are valid and not password-protected.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mergedPdfBytes = await newPdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'merged.pdf'
|
||||||
|
);
|
||||||
|
showAlert('Success', 'PDFs merged successfully!');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Merge error:', e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupMergeTool() {
|
export async function setupMergeTool() {
|
||||||
document.getElementById('merge-options').classList.remove('hidden');
|
document.getElementById('merge-options').classList.remove('hidden');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @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('process-btn').disabled = false;
|
document.getElementById('process-btn').disabled = false;
|
||||||
|
|
||||||
const wasInPageMode = mergeState.activeMode === 'page';
|
const wasInPageMode = mergeState.activeMode === 'page';
|
||||||
|
|
||||||
showLoader('Loading PDF documents...');
|
showLoader('Loading PDF documents...');
|
||||||
try {
|
try {
|
||||||
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) {
|
);
|
||||||
console.error('Error loading PDFs:', error);
|
}
|
||||||
showAlert('Error', 'Failed to load one or more PDF files');
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading PDFs:', error);
|
||||||
|
showAlert('Error', 'Failed to load one or more PDF files');
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
|
||||||
const fileModeBtn = document.getElementById('file-mode-btn');
|
const fileModeBtn = document.getElementById('file-mode-btn');
|
||||||
const pageModeBtn = document.getElementById('page-mode-btn');
|
const pageModeBtn = document.getElementById('page-mode-btn');
|
||||||
const filePanel = document.getElementById('file-mode-panel');
|
const filePanel = document.getElementById('file-mode-panel');
|
||||||
const pagePanel = document.getElementById('page-mode-panel');
|
const pagePanel = document.getElementById('page-mode-panel');
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
|
|
||||||
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 =
|
||||||
li.dataset.fileName = f.name;
|
'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
||||||
|
li.dataset.fileName = f.name;
|
||||||
|
|
||||||
const mainDiv = document.createElement('div');
|
const mainDiv = document.createElement('div');
|
||||||
mainDiv.className = 'flex items-center justify-between';
|
mainDiv.className = 'flex items-center justify-between';
|
||||||
|
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 'truncate font-medium text-white flex-1 mr-2';
|
nameSpan.className = 'truncate font-medium text-white flex-1 mr-2';
|
||||||
nameSpan.title = f.name;
|
nameSpan.title = f.name;
|
||||||
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 =
|
||||||
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
|
'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
|
||||||
|
|
||||||
mainDiv.append(nameSpan, dragHandle);
|
mainDiv.append(nameSpan, dragHandle);
|
||||||
|
|
||||||
const rangeDiv = document.createElement('div');
|
const rangeDiv = document.createElement('div');
|
||||||
rangeDiv.className = 'mt-2';
|
rangeDiv.className = 'mt-2';
|
||||||
|
|
||||||
const label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
label.htmlFor = `range-${safeFileName}`;
|
label.htmlFor = `range-${safeFileName}`;
|
||||||
label.className = 'text-xs text-gray-400';
|
label.className = 'text-xs text-gray-400';
|
||||||
label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`;
|
label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`;
|
||||||
|
|
||||||
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 =
|
||||||
input.placeholder = 'Leave blank for all pages';
|
'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
||||||
|
input.placeholder = 'Leave blank for all pages';
|
||||||
|
|
||||||
rangeDiv.append(label, input);
|
rangeDiv.append(label, input);
|
||||||
li.append(mainDiv, rangeDiv);
|
li.append(mainDiv, rangeDiv);
|
||||||
fileList.appendChild(li);
|
fileList.appendChild(li);
|
||||||
});
|
});
|
||||||
|
|
||||||
initializeFileListSortable();
|
initializeFileListSortable();
|
||||||
|
|
||||||
const newFileModeBtn = fileModeBtn.cloneNode(true);
|
const newFileModeBtn = fileModeBtn.cloneNode(true);
|
||||||
const newPageModeBtn = pageModeBtn.cloneNode(true);
|
const newPageModeBtn = pageModeBtn.cloneNode(true);
|
||||||
fileModeBtn.replaceWith(newFileModeBtn);
|
fileModeBtn.replaceWith(newFileModeBtn);
|
||||||
pageModeBtn.replaceWith(newPageModeBtn);
|
pageModeBtn.replaceWith(newPageModeBtn);
|
||||||
|
|
||||||
newFileModeBtn.addEventListener('click', () => {
|
newFileModeBtn.addEventListener('click', () => {
|
||||||
if (mergeState.activeMode === 'file') return;
|
if (mergeState.activeMode === 'file') return;
|
||||||
|
|
||||||
mergeState.activeMode = 'file';
|
mergeState.activeMode = 'file';
|
||||||
filePanel.classList.remove('hidden');
|
filePanel.classList.remove('hidden');
|
||||||
pagePanel.classList.add('hidden');
|
pagePanel.classList.add('hidden');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
newFileModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
newPageModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
});
|
});
|
||||||
|
|
||||||
newPageModeBtn.addEventListener('click', async () => {
|
newPageModeBtn.addEventListener('click', async () => {
|
||||||
if (mergeState.activeMode === 'page') return;
|
if (mergeState.activeMode === 'page') return;
|
||||||
|
|
||||||
mergeState.activeMode = 'page';
|
mergeState.activeMode = 'page';
|
||||||
filePanel.classList.add('hidden');
|
filePanel.classList.add('hidden');
|
||||||
pagePanel.classList.remove('hidden');
|
pagePanel.classList.remove('hidden');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
|
|
||||||
await renderPageMergeThumbnails();
|
await renderPageMergeThumbnails();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (wasInPageMode) {
|
if (wasInPageMode) {
|
||||||
mergeState.activeMode = 'page';
|
mergeState.activeMode = 'page';
|
||||||
filePanel.classList.add('hidden');
|
filePanel.classList.add('hidden');
|
||||||
pagePanel.classList.remove('hidden');
|
pagePanel.classList.remove('hidden');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
newPageModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
newPageModeBtn.classList.remove('bg-gray-700', 'text-gray-300');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
newFileModeBtn.classList.remove('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
|
|
||||||
await renderPageMergeThumbnails();
|
await renderPageMergeThumbnails();
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
newFileModeBtn.classList.add('bg-indigo-600', 'text-white');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'classList' does not exist on type 'Node'... Remove this comment to see the full error message
|
||||||
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+112
-95
@@ -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';
|
||||||
@@ -6,106 +5,124 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
|
||||||
|
|
||||||
export function setupNUpUI() {
|
export function setupNUpUI() {
|
||||||
const addBorderCheckbox = document.getElementById('add-border');
|
const addBorderCheckbox = document.getElementById('add-border');
|
||||||
const borderColorWrapper = document.getElementById('border-color-wrapper');
|
const borderColorWrapper = document.getElementById('border-color-wrapper');
|
||||||
if (addBorderCheckbox && borderColorWrapper) {
|
if (addBorderCheckbox && borderColorWrapper) {
|
||||||
addBorderCheckbox.addEventListener('change', () => {
|
addBorderCheckbox.addEventListener('change', () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||||
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
|
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function nUpTool() {
|
export async function nUpTool() {
|
||||||
// 1. Gather all options from the UI
|
// 1. Gather all options from the UI
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const n = parseInt(document.getElementById('pages-per-sheet').value);
|
const n = parseInt(document.getElementById('pages-per-sheet').value);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const pageSizeKey = document.getElementById('output-page-size').value;
|
const pageSizeKey = document.getElementById('output-page-size').value;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
let orientation = document.getElementById('output-orientation').value;
|
let orientation = document.getElementById('output-orientation').value;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||||
const useMargins = document.getElementById('add-margins').checked;
|
const useMargins = document.getElementById('add-margins').checked;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||||
const addBorder = document.getElementById('add-border').checked;
|
const addBorder = document.getElementById('add-border').checked;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const borderColor = hexToRgb(document.getElementById('border-color').value);
|
const borderColor = hexToRgb(document.getElementById('border-color').value);
|
||||||
|
|
||||||
showLoader('Creating N-Up PDF...');
|
showLoader('Creating N-Up PDF...');
|
||||||
try {
|
try {
|
||||||
const sourceDoc = state.pdfDoc;
|
const sourceDoc = state.pdfDoc;
|
||||||
const newDoc = await PDFLibDocument.create();
|
const newDoc = await PDFLibDocument.create();
|
||||||
const sourcePages = sourceDoc.getPages();
|
const sourcePages = sourceDoc.getPages();
|
||||||
|
|
||||||
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
|
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
|
||||||
|
|
||||||
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
|
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
|
||||||
|
|
||||||
if (orientation === 'auto') {
|
if (orientation === 'auto') {
|
||||||
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]
|
||||||
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
? 'landscape'
|
||||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
: 'portrait';
|
||||||
}
|
|
||||||
|
|
||||||
const margin = useMargins ? 36 : 0;
|
|
||||||
const gutter = useMargins ? 10 : 0;
|
|
||||||
|
|
||||||
const usableWidth = pageWidth - (margin * 2);
|
|
||||||
const usableHeight = pageHeight - (margin * 2);
|
|
||||||
|
|
||||||
// Loop through the source pages in chunks of 'n'
|
|
||||||
for (let i = 0; i < sourcePages.length; i += n) {
|
|
||||||
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
|
|
||||||
const chunk = sourcePages.slice(i, i + n);
|
|
||||||
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
|
||||||
|
|
||||||
// Calculate dimensions of each cell in the grid
|
|
||||||
const cellWidth = (usableWidth - (gutter * (gridDims[0] - 1))) / gridDims[0];
|
|
||||||
const cellHeight = (usableHeight - (gutter * (gridDims[1] - 1))) / gridDims[1];
|
|
||||||
|
|
||||||
for (let j = 0; j < chunk.length; j++) {
|
|
||||||
const sourcePage = chunk[j];
|
|
||||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
|
||||||
|
|
||||||
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
|
||||||
const scale = Math.min(cellWidth / embeddedPage.width, cellHeight / embeddedPage.height);
|
|
||||||
const scaledWidth = embeddedPage.width * scale;
|
|
||||||
const scaledHeight = embeddedPage.height * scale;
|
|
||||||
|
|
||||||
// Calculate position (x, y) for this cell
|
|
||||||
const row = Math.floor(j / gridDims[0]);
|
|
||||||
const col = j % gridDims[0];
|
|
||||||
const cellX = margin + col * (cellWidth + gutter);
|
|
||||||
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
|
||||||
|
|
||||||
// Center the page within its cell
|
|
||||||
const x = cellX + (cellWidth - scaledWidth) / 2;
|
|
||||||
const y = cellY + (cellHeight - scaledHeight) / 2;
|
|
||||||
|
|
||||||
outputPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
|
||||||
|
|
||||||
if (addBorder) {
|
|
||||||
outputPage.drawRectangle({
|
|
||||||
x, y,
|
|
||||||
width: scaledWidth,
|
|
||||||
height: scaledHeight,
|
|
||||||
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
|
||||||
borderWidth: 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), `n-up_${n}.pdf`);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
||||||
|
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||||
|
}
|
||||||
|
|
||||||
|
const margin = useMargins ? 36 : 0;
|
||||||
|
const gutter = useMargins ? 10 : 0;
|
||||||
|
|
||||||
|
const usableWidth = pageWidth - margin * 2;
|
||||||
|
const usableHeight = pageHeight - margin * 2;
|
||||||
|
|
||||||
|
// Loop through the source pages in chunks of 'n'
|
||||||
|
for (let i = 0; i < sourcePages.length; i += n) {
|
||||||
|
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
|
||||||
|
const chunk = sourcePages.slice(i, i + n);
|
||||||
|
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
||||||
|
|
||||||
|
// Calculate dimensions of each cell in the grid
|
||||||
|
const cellWidth =
|
||||||
|
(usableWidth - gutter * (gridDims[0] - 1)) / gridDims[0];
|
||||||
|
const cellHeight =
|
||||||
|
(usableHeight - gutter * (gridDims[1] - 1)) / gridDims[1];
|
||||||
|
|
||||||
|
for (let j = 0; j < chunk.length; j++) {
|
||||||
|
const sourcePage = chunk[j];
|
||||||
|
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||||
|
|
||||||
|
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
||||||
|
const scale = Math.min(
|
||||||
|
cellWidth / embeddedPage.width,
|
||||||
|
cellHeight / embeddedPage.height
|
||||||
|
);
|
||||||
|
const scaledWidth = embeddedPage.width * scale;
|
||||||
|
const scaledHeight = embeddedPage.height * scale;
|
||||||
|
|
||||||
|
// Calculate position (x, y) for this cell
|
||||||
|
const row = Math.floor(j / gridDims[0]);
|
||||||
|
const col = j % gridDims[0];
|
||||||
|
const cellX = margin + col * (cellWidth + gutter);
|
||||||
|
const cellY =
|
||||||
|
pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
||||||
|
|
||||||
|
// Center the page within its cell
|
||||||
|
const x = cellX + (cellWidth - scaledWidth) / 2;
|
||||||
|
const y = cellY + (cellHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
|
outputPage.drawPage(embeddedPage, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: scaledWidth,
|
||||||
|
height: scaledHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (addBorder) {
|
||||||
|
outputPage.drawRectangle({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: scaledWidth,
|
||||||
|
height: scaledHeight,
|
||||||
|
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
||||||
|
borderWidth: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdfBytes = await newDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
`n-up_${n}.pdf`
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+289
-219
@@ -4,275 +4,345 @@ 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(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
|
||||||
|
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseHOCR(hocrText: string) {
|
function parseHOCR(hocrText: string) {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(hocrText, 'text/html');
|
const doc = parser.parseFromString(hocrText, 'text/html');
|
||||||
const words = [];
|
const words = [];
|
||||||
|
|
||||||
// Find all word elements in hOCR
|
// Find all word elements in hOCR
|
||||||
const wordElements = doc.querySelectorAll('.ocrx_word');
|
const wordElements = doc.querySelectorAll('.ocrx_word');
|
||||||
|
|
||||||
wordElements.forEach((wordEl) => {
|
wordElements.forEach((wordEl) => {
|
||||||
const titleAttr = wordEl.getAttribute('title');
|
const titleAttr = wordEl.getAttribute('title');
|
||||||
const text = wordEl.textContent?.trim() || '';
|
const text = wordEl.textContent?.trim() || '';
|
||||||
|
|
||||||
if (!titleAttr || !text) return;
|
if (!titleAttr || !text) return;
|
||||||
|
|
||||||
// Parse bbox coordinates from title attribute
|
// Parse bbox coordinates from title attribute
|
||||||
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
|
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
|
||||||
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
|
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
|
||||||
const confMatch = titleAttr.match(/x_wconf (\d+)/);
|
const confMatch = titleAttr.match(/x_wconf (\d+)/);
|
||||||
|
|
||||||
if (bboxMatch) {
|
if (bboxMatch) {
|
||||||
words.push({
|
words.push({
|
||||||
text: text,
|
text: text,
|
||||||
bbox: {
|
bbox: {
|
||||||
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return words;
|
return words;
|
||||||
}
|
}
|
||||||
|
|
||||||
function binarizeCanvas(ctx: any) {
|
function binarizeCanvas(ctx: any) {
|
||||||
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
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 =
|
||||||
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||||||
data[i] = data[i + 1] = data[i + 2] = color;
|
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
||||||
}
|
data[i] = data[i + 1] = data[i + 2] = color;
|
||||||
ctx.putImageData(imageData, 0, 0);
|
}
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProgress(status: any, progress: any) {
|
function updateProgress(status: any, progress: any) {
|
||||||
const progressBar = document.getElementById('progress-bar');
|
const progressBar = document.getElementById('progress-bar');
|
||||||
const progressStatus = document.getElementById('progress-status');
|
const progressStatus = document.getElementById('progress-status');
|
||||||
const progressLog = document.getElementById('progress-log');
|
const progressLog = document.getElementById('progress-log');
|
||||||
|
|
||||||
if (!progressBar || !progressStatus || !progressLog) return;
|
if (!progressBar || !progressStatus || !progressLog) return;
|
||||||
|
|
||||||
progressStatus.textContent = status;
|
progressStatus.textContent = status;
|
||||||
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
|
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
|
||||||
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
|
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
|
||||||
|
|
||||||
const logMessage = `Status: ${status}`;
|
const logMessage = `Status: ${status}`;
|
||||||
progressLog.textContent += logMessage + '\n';
|
progressLog.textContent += logMessage + '\n';
|
||||||
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')
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
).map((cb) => (cb as HTMLInputElement).value);
|
||||||
const scale = parseFloat(document.getElementById('ocr-resolution').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 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
const scale = parseFloat(document.getElementById('ocr-resolution').value);
|
||||||
const binarize = document.getElementById('ocr-binarize').checked;
|
// @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 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const binarize = document.getElementById('ocr-binarize').checked;
|
||||||
const whitelist = document.getElementById('ocr-whitelist').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 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(
|
||||||
return;
|
'No Languages Selected',
|
||||||
}
|
'Please select at least one language for OCR.'
|
||||||
const langString = selectedLangs.join('+');
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const langString = selectedLangs.join('+');
|
||||||
|
|
||||||
document.getElementById('ocr-options').classList.add('hidden');
|
document.getElementById('ocr-options').classList.add('hidden');
|
||||||
document.getElementById('ocr-progress').classList.remove('hidden');
|
document.getElementById('ocr-progress').classList.remove('hidden');
|
||||||
|
|
||||||
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
|
||||||
await worker.setParameters({
|
await worker.setParameters({
|
||||||
tessjs_create_hocr: '1',
|
tessjs_create_hocr: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (whitelist.trim()) {
|
await worker.setParameters({
|
||||||
await worker.setParameters({
|
tessedit_char_whitelist: whitelist,
|
||||||
tessedit_char_whitelist: whitelist.trim(),
|
});
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
|
let fullText = '';
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
updateProgress(
|
||||||
|
`Processing page ${i} of ${pdf.numPages}`,
|
||||||
|
(i - 1) / pdf.numPages
|
||||||
|
);
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const viewport = page.getViewport({ scale });
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
|
|
||||||
|
if (binarize) {
|
||||||
|
binarizeCanvas(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await worker.recognize(
|
||||||
|
canvas,
|
||||||
|
{},
|
||||||
|
{ text: true, hocr: true }
|
||||||
|
);
|
||||||
|
const data = result.data;
|
||||||
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
|
const pngImageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
|
reader.readAsArrayBuffer(blob);
|
||||||
|
}, 'image/png')
|
||||||
|
);
|
||||||
|
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||||
|
newPage.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse hOCR to get word-level data
|
||||||
|
if (data.hocr) {
|
||||||
|
const words = parseHOCR(data.hocr);
|
||||||
|
|
||||||
|
words.forEach((word: any) => {
|
||||||
|
const { x0, y0, x1, y1 } = word.bbox;
|
||||||
|
// Sanitize the text to remove characters WinAnsi cannot encode
|
||||||
|
const text = sanitizeTextForWinAnsi(word.text);
|
||||||
|
|
||||||
|
// Skip words that become empty after sanitization
|
||||||
|
if (!text.trim()) return;
|
||||||
|
|
||||||
|
const bboxWidth = x1 - x0;
|
||||||
|
const bboxHeight = y1 - y0;
|
||||||
|
|
||||||
|
let fontSize = bboxHeight * 0.9;
|
||||||
|
let textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||||
|
while (textWidth > bboxWidth && fontSize > 1) {
|
||||||
|
fontSize -= 0.5;
|
||||||
|
textWidth = font.widthOfTextAtSize(text, fontSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
newPage.drawText(text, {
|
||||||
|
x: x0,
|
||||||
|
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
|
||||||
|
font,
|
||||||
|
size: fontSize,
|
||||||
|
color: rgb(0, 0, 0),
|
||||||
|
opacity: 0,
|
||||||
});
|
});
|
||||||
}
|
} catch (error) {
|
||||||
|
// If drawing fails despite sanitization, log and skip this word
|
||||||
|
console.warn(`Could not draw text "${text}":`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
fullText += data.text + '\n\n';
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
}
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
|
||||||
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
|
||||||
let fullText = '';
|
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
await worker.terminate();
|
||||||
updateProgress(`Processing page ${i} of ${pdf.numPages}`, (i - 1) / pdf.numPages);
|
|
||||||
const page = await pdf.getPage(i);
|
|
||||||
const viewport = page.getViewport({ scale });
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = viewport.width;
|
|
||||||
canvas.height = viewport.height;
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
|
||||||
|
|
||||||
if (binarize) {
|
searchablePdfBytes = await newPdfDoc.save();
|
||||||
binarizeCanvas(context);
|
document.getElementById('ocr-progress').classList.add('hidden');
|
||||||
}
|
document.getElementById('ocr-results').classList.remove('hidden');
|
||||||
|
|
||||||
const result = await worker.recognize(canvas);
|
createIcons({ icons });
|
||||||
const data = result.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 newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
document.getElementById('ocr-text-output').value = fullText.trim();
|
||||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
|
||||||
reader.readAsArrayBuffer(blob);
|
|
||||||
}, 'image/png'));
|
|
||||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
|
||||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
|
||||||
|
|
||||||
// Parse hOCR to get word-level data
|
document
|
||||||
if (data.hocr) {
|
.getElementById('download-searchable-pdf')
|
||||||
const words = parseHOCR(data.hocr);
|
.addEventListener('click', () => {
|
||||||
|
downloadFile(
|
||||||
|
new Blob([searchablePdfBytes], { type: 'application/pdf' }),
|
||||||
|
'searchable.pdf'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
words.forEach((word: any) => {
|
// CHANGE: The copy button logic is updated to be safer.
|
||||||
const { x0, y0, x1, y1 } = word.bbox;
|
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
|
||||||
// Sanitize the text to remove characters WinAnsi cannot encode
|
const button = e.currentTarget as HTMLButtonElement;
|
||||||
const text = sanitizeTextForWinAnsi(word.text);
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme...
|
||||||
|
const textToCopy = document.getElementById('ocr-text-output').value;
|
||||||
|
|
||||||
// Skip words that become empty after sanitization
|
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||||
if (!text.trim()) return;
|
button.textContent = ''; // Clear the button safely
|
||||||
|
const icon = document.createElement('i');
|
||||||
|
icon.setAttribute('data-lucide', 'check');
|
||||||
|
icon.className = 'w-4 h-4 text-green-400';
|
||||||
|
button.appendChild(icon);
|
||||||
|
createIcons({ icons });
|
||||||
|
|
||||||
const bboxWidth = x1 - x0;
|
setTimeout(() => {
|
||||||
const bboxHeight = y1 - y0;
|
const currentButton = document.getElementById('copy-text-btn');
|
||||||
|
if (currentButton) {
|
||||||
|
currentButton.textContent = ''; // Clear the button safely
|
||||||
|
const resetIcon = document.createElement('i');
|
||||||
|
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
|
||||||
|
resetIcon.className = 'w-4 h-4 text-gray-300';
|
||||||
|
currentButton.appendChild(resetIcon);
|
||||||
|
createIcons({ icons });
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
let fontSize = bboxHeight * 0.9;
|
document
|
||||||
let textWidth = font.widthOfTextAtSize(text, fontSize);
|
.getElementById('download-txt-btn')
|
||||||
while (textWidth > bboxWidth && fontSize > 1) {
|
.addEventListener('click', () => {
|
||||||
fontSize -= 0.5;
|
|
||||||
textWidth = font.widthOfTextAtSize(text, fontSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
newPage.drawText(text, {
|
|
||||||
x: x0,
|
|
||||||
y: viewport.height - y1 + (bboxHeight - fontSize) / 2,
|
|
||||||
font,
|
|
||||||
size: fontSize,
|
|
||||||
color: rgb(0, 0, 0),
|
|
||||||
opacity: 0,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// If drawing fails despite sanitization, log and skip this word
|
|
||||||
console.warn(`Could not draw text "${text}":`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fullText += data.text + '\n\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
await worker.terminate();
|
|
||||||
|
|
||||||
searchablePdfBytes = await newPdfDoc.save();
|
|
||||||
document.getElementById('ocr-progress').classList.add('hidden');
|
|
||||||
document.getElementById('ocr-results').classList.remove('hidden');
|
|
||||||
|
|
||||||
createIcons({icons});
|
|
||||||
// @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();
|
const textToSave = document.getElementById('ocr-text-output').value;
|
||||||
|
const blob = new Blob([textToSave], { type: 'text/plain' });
|
||||||
document.getElementById('download-searchable-pdf').addEventListener('click', () => {
|
downloadFile(blob, 'ocr-text.txt');
|
||||||
downloadFile(new Blob([searchablePdfBytes], { type: 'application/pdf' }), 'searchable.pdf');
|
});
|
||||||
});
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
// CHANGE: The copy button logic is updated to be safer.
|
showAlert(
|
||||||
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
|
'OCR Error',
|
||||||
const button = e.currentTarget as HTMLButtonElement;
|
'An error occurred during the OCR process. The worker may have failed to load. Please try again.'
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme...
|
);
|
||||||
const textToCopy = document.getElementById('ocr-text-output').value;
|
document.getElementById('ocr-options').classList.remove('hidden');
|
||||||
|
document.getElementById('ocr-progress').classList.add('hidden');
|
||||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
}
|
||||||
button.textContent = ''; // Clear the button safely
|
|
||||||
const icon = document.createElement('i');
|
|
||||||
icon.setAttribute('data-lucide', 'check');
|
|
||||||
icon.className = 'w-4 h-4 text-green-400';
|
|
||||||
button.appendChild(icon);
|
|
||||||
createIcons({ icons });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
const currentButton = document.getElementById('copy-text-btn');
|
|
||||||
if (currentButton) {
|
|
||||||
currentButton.textContent = ''; // Clear the button safely
|
|
||||||
const resetIcon = document.createElement('i');
|
|
||||||
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
|
|
||||||
resetIcon.className = 'w-4 h-4 text-gray-300';
|
|
||||||
currentButton.appendChild(resetIcon);
|
|
||||||
createIcons({ icons });
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('download-txt-btn').addEventListener('click', () => {
|
|
||||||
// @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 blob = new Blob([textToSave], { type: 'text/plain' });
|
|
||||||
downloadFile(blob, 'ocr-text.txt');
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('OCR Error', 'An error occurred during the OCR process. The worker may have failed to load. Please try again.');
|
|
||||||
document.getElementById('ocr-options').classList.remove('hidden');
|
|
||||||
document.getElementById('ocr-progress').classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the UI and event listeners for the OCR tool.
|
* 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(
|
||||||
const processBtn = document.getElementById('process-btn');
|
'selected-langs-display'
|
||||||
|
);
|
||||||
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
langSearch.addEventListener('input', () => {
|
// Whitelist presets
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const whitelistPresets: Record<string, string> = {
|
||||||
const searchTerm = langSearch.value.toLowerCase();
|
alphanumeric:
|
||||||
langList.querySelectorAll('label').forEach(label => {
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"',
|
||||||
label.style.display = label.textContent.toLowerCase().includes(searchTerm) ? '' : 'none';
|
'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)';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Update the display of selected languages
|
langSearch.addEventListener('input', () => {
|
||||||
langList.addEventListener('change', () => {
|
const searchTerm = (langSearch as HTMLInputElement).value.toLowerCase();
|
||||||
const selected = Array.from(langList.querySelectorAll('.lang-checkbox:checked'))
|
langList.querySelectorAll('label').forEach((label) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
label.style.display = label.textContent.toLowerCase().includes(searchTerm)
|
||||||
.map(cb => tesseractLanguages[cb.value]);
|
? ''
|
||||||
selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None';
|
: 'none';
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
|
||||||
processBtn.disabled = selected.length === 0;
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Attach the main OCR function to the process button
|
langList.addEventListener('change', () => {
|
||||||
processBtn.addEventListener('click', runOCR);
|
const selected = Array.from(
|
||||||
|
langList.querySelectorAll('.lang-checkbox:checked')
|
||||||
|
).map((cb) => tesseractLanguages[(cb as HTMLInputElement).value]);
|
||||||
|
selectedLangsDisplay.textContent =
|
||||||
|
selected.length > 0 ? selected.join(', ') : 'None';
|
||||||
|
(processBtn as HTMLButtonElement).disabled = selected.length === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
processBtn.addEventListener('click', runOCR);
|
||||||
}
|
}
|
||||||
+20
-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';
|
||||||
@@ -6,22 +5,26 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function organize() {
|
export async function organize() {
|
||||||
showLoader('Saving changes...');
|
showLoader('Saving changes...');
|
||||||
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(
|
||||||
} catch (e) {
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
console.error(e);
|
'organized.pdf'
|
||||||
showAlert('Error', 'Could not save the changes.');
|
);
|
||||||
} finally {
|
} catch (e) {
|
||||||
hideLoader();
|
console.error(e);
|
||||||
}
|
showAlert('Error', 'Could not save the changes.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,37 +8,37 @@ let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing
|
|||||||
* @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px').
|
* @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px').
|
||||||
*/
|
*/
|
||||||
function renderTable(unit: any) {
|
function renderTable(unit: any) {
|
||||||
const tableBody = document.getElementById('dimensions-table-body');
|
const tableBody = document.getElementById('dimensions-table-body');
|
||||||
if (!tableBody) return;
|
if (!tableBody) return;
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
// Create and append each cell safely using textContent
|
// Create and append each cell safely using textContent
|
||||||
const pageNumCell = document.createElement('td');
|
const pageNumCell = document.createElement('td');
|
||||||
pageNumCell.className = 'px-4 py-3 text-white';
|
pageNumCell.className = 'px-4 py-3 text-white';
|
||||||
pageNumCell.textContent = pageData.pageNum;
|
pageNumCell.textContent = pageData.pageNum;
|
||||||
|
|
||||||
const dimensionsCell = document.createElement('td');
|
const dimensionsCell = document.createElement('td');
|
||||||
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
dimensionsCell.className = 'px-4 py-3 text-gray-300';
|
||||||
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
|
||||||
|
|
||||||
const sizeCell = document.createElement('td');
|
const sizeCell = document.createElement('td');
|
||||||
sizeCell.className = 'px-4 py-3 text-gray-300';
|
sizeCell.className = 'px-4 py-3 text-gray-300';
|
||||||
sizeCell.textContent = pageData.standardSize;
|
sizeCell.textContent = pageData.standardSize;
|
||||||
|
|
||||||
const orientationCell = document.createElement('td');
|
const orientationCell = document.createElement('td');
|
||||||
orientationCell.className = 'px-4 py-3 text-gray-300';
|
orientationCell.className = 'px-4 py-3 text-gray-300';
|
||||||
orientationCell.textContent = pageData.orientation;
|
orientationCell.textContent = pageData.orientation;
|
||||||
|
|
||||||
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
|
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,35 +46,35 @@ function renderTable(unit: any) {
|
|||||||
* This is called once after the file is loaded.
|
* This is called once after the file is loaded.
|
||||||
*/
|
*/
|
||||||
export function analyzeAndDisplayDimensions() {
|
export function analyzeAndDisplayDimensions() {
|
||||||
if (!state.pdfDoc) return;
|
if (!state.pdfDoc) return;
|
||||||
|
|
||||||
analyzedPagesData = []; // Reset stored data
|
analyzedPagesData = []; // Reset stored data
|
||||||
const pages = state.pdfDoc.getPages();
|
const pages = state.pdfDoc.getPages();
|
||||||
|
|
||||||
pages.forEach((page: any, index: any) => {
|
pages.forEach((page: any, index: any) => {
|
||||||
const { width, height } = page.getSize();
|
const { width, height } = page.getSize();
|
||||||
analyzedPagesData.push({
|
analyzedPagesData.push({
|
||||||
pageNum: index + 1,
|
pageNum: index + 1,
|
||||||
width, // Store raw width in points
|
width, // Store raw width in points
|
||||||
height, // Store raw height in points
|
height, // Store raw height in points
|
||||||
orientation: width > height ? 'Landscape' : 'Portrait',
|
orientation: width > height ? 'Landscape' : 'Portrait',
|
||||||
standardSize: getStandardPageName(width, height),
|
standardSize: getStandardPageName(width, height),
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const resultsContainer = document.getElementById('dimensions-results');
|
const resultsContainer = document.getElementById('dimensions-results');
|
||||||
const unitsSelect = document.getElementById('units-select');
|
const unitsSelect = document.getElementById('units-select');
|
||||||
|
|
||||||
// Initial render with default unit (points)
|
// Initial render with default unit (points)
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
renderTable(unitsSelect.value);
|
renderTable(unitsSelect.value);
|
||||||
|
|
||||||
// Show the results table
|
// Show the results table
|
||||||
resultsContainer.classList.remove('hidden');
|
resultsContainer.classList.remove('hidden');
|
||||||
|
|
||||||
// Add event listener to handle unit changes
|
// Add event listener to handle unit changes
|
||||||
unitsSelect.addEventListener('change', (e) => {
|
unitsSelect.addEventListener('change', (e) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||||
renderTable(e.target.value);
|
renderTable(e.target.value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+70
-67
@@ -10,84 +10,87 @@ import JSZip from 'jszip';
|
|||||||
* @returns {ArrayBuffer} The complete BMP file as an ArrayBuffer.
|
* @returns {ArrayBuffer} The complete BMP file as an ArrayBuffer.
|
||||||
*/
|
*/
|
||||||
function encodeBMP(imageData: any) {
|
function encodeBMP(imageData: any) {
|
||||||
const { width, height, data } = imageData;
|
const { width, height, data } = imageData;
|
||||||
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
|
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
|
||||||
const fileSize = stride * height + 54; // 54 byte header
|
const fileSize = stride * height + 54; // 54 byte header
|
||||||
const buffer = new ArrayBuffer(fileSize);
|
const buffer = new ArrayBuffer(fileSize);
|
||||||
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
|
||||||
|
|
||||||
// DIB Header (BITMAPINFOHEADER) (40 bytes)
|
// DIB Header (BITMAPINFOHEADER) (40 bytes)
|
||||||
view.setUint32(14, 40, true); // DIB header size
|
view.setUint32(14, 40, true); // DIB header size
|
||||||
view.setUint32(18, width, true);
|
view.setUint32(18, width, true);
|
||||||
view.setUint32(22, -height, true); // Negative height for top-down scanline order
|
view.setUint32(22, -height, true); // Negative height for top-down scanline order
|
||||||
view.setUint16(26, 1, true); // Color planes
|
view.setUint16(26, 1, true); // Color planes
|
||||||
view.setUint16(28, 24, true); // Bits per pixel
|
view.setUint16(28, 24, true); // Bits per pixel
|
||||||
view.setUint32(30, 0, true); // No compression
|
view.setUint32(30, 0, true); // No compression
|
||||||
view.setUint32(34, stride * height, true); // Image size
|
view.setUint32(34, stride * height, true); // Image size
|
||||||
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
|
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
|
||||||
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
|
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
|
||||||
|
|
||||||
// Pixel Data
|
// Pixel Data
|
||||||
let offset = 54;
|
let offset = 54;
|
||||||
for (let y = 0; y < height; y++) {
|
for (let y = 0; y < height; y++) {
|
||||||
for (let x = 0; x < width; x++) {
|
for (let x = 0; x < width; x++) {
|
||||||
const i = (y * width + x) * 4;
|
const i = (y * width + x) * 4;
|
||||||
// BMP is BGR, not RGB
|
// BMP is BGR, not RGB
|
||||||
view.setUint8(offset++, data[i + 2]); // Blue
|
view.setUint8(offset++, data[i + 2]); // Blue
|
||||||
view.setUint8(offset++, data[i + 1]); // Green
|
view.setUint8(offset++, data[i + 1]); // Green
|
||||||
view.setUint8(offset++, data[i]); // Red
|
view.setUint8(offset++, data[i]); // Red
|
||||||
}
|
|
||||||
// Add padding to make the row a multiple of 4 bytes
|
|
||||||
for (let p = 0; p < (stride - width * 3); p++) {
|
|
||||||
view.setUint8(offset++, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return buffer;
|
// Add padding to make the row a multiple of 4 bytes
|
||||||
|
for (let p = 0; p < stride - width * 3; p++) {
|
||||||
|
view.setUint8(offset++, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function pdfToBmp() {
|
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(
|
||||||
const zip = new JSZip();
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
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}...`);
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const viewport = page.getViewport({ scale: 1.5 });
|
const viewport = page.getViewport({ scale: 1.5 });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
// Render the PDF page directly to the canvas
|
// Render the PDF page directly to the canvas
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
|
|
||||||
// Get the raw pixel data from this canvas
|
// Get the raw pixel data from this canvas
|
||||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
// Use our new self-contained function to create the BMP file
|
// Use our new self-contained function to create the BMP file
|
||||||
const bmpBuffer = encodeBMP(imageData);
|
const bmpBuffer = encodeBMP(imageData);
|
||||||
|
|
||||||
// Add the generated BMP file to the zip archive
|
// Add the generated BMP file to the zip archive
|
||||||
zip.file(`page_${i}.bmp`, bmpBuffer);
|
zip.file(`page_${i}.bmp`, bmpBuffer);
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Compressing files into a ZIP...');
|
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
|
||||||
downloadFile(zipBlob, 'converted_bmp_images.zip');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to convert PDF to BMP. The file might be corrupted.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showLoader('Compressing files into a ZIP...');
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'converted_bmp_images.zip');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert PDF to BMP. The file might be corrupted.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,54 +5,64 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function pdfToGreyscale() {
|
export async function pdfToGreyscale() {
|
||||||
if (!state.pdfDoc) {
|
if (!state.pdfDoc) {
|
||||||
showAlert('Error', 'PDF not loaded.');
|
showAlert('Error', 'PDF not loaded.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Converting to greyscale...');
|
showLoader('Converting to greyscale...');
|
||||||
try {
|
try {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||||
const page = await pdfjsDoc.getPage(i);
|
const page = await pdfjsDoc.getPage(i);
|
||||||
const viewport = page.getViewport({ scale: 1.5 });
|
const viewport = page.getViewport({ scale: 1.5 });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
await page.render({ canvasContext: ctx, viewport }).promise;
|
||||||
|
|
||||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
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) {
|
||||||
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
|
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
|
||||||
data[j] = avg; // red
|
data[j] = avg; // red
|
||||||
data[j + 1] = avg; // green
|
data[j + 1] = avg; // green
|
||||||
data[j + 2] = avg; // blue
|
data[j + 2] = avg; // blue
|
||||||
}
|
}
|
||||||
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) =>
|
||||||
const reader = new FileReader();
|
canvas.toBlob((blob) => {
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
const reader = new FileReader();
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
}, 'image/png'));
|
reader.readAsArrayBuffer(blob);
|
||||||
|
}, '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,
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
y: 0,
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'greyscale.pdf');
|
width: image.width,
|
||||||
} catch (e) {
|
height: image.height,
|
||||||
console.error(e);
|
});
|
||||||
showAlert('Error', 'Could not convert to greyscale.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'greyscale.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not convert to greyscale.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+31
-24
@@ -4,32 +4,39 @@ import { state } from '../state.js';
|
|||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
export async function pdfToJpg() {
|
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(
|
||||||
const zip = new JSZip();
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
|
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);
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
const viewport = page.getViewport({ scale: 2.0 });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
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) =>
|
||||||
zip.file(`page_${i}.jpg`, blob as Blob);
|
canvas.toBlob(resolve, 'image/jpeg', 0.9)
|
||||||
}
|
);
|
||||||
|
zip.file(`page_${i}.jpg`, blob as Blob);
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
|
||||||
downloadFile(zipBlob, 'converted_images.zip');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to convert PDF to JPG. The file might be corrupted.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'converted_images.zip');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert PDF to JPG. The file might be corrupted.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,33 @@
|
|||||||
|
|
||||||
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';
|
||||||
|
|
||||||
export async function pdfToMarkdown() {
|
export async function pdfToMarkdown() {
|
||||||
showLoader('Converting to Markdown...');
|
showLoader('Converting to Markdown...');
|
||||||
try {
|
try {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
// @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({ data: arrayBuffer }).promise;
|
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||||
let markdown = '';
|
let markdown = '';
|
||||||
|
|
||||||
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);
|
||||||
const content = await page.getTextContent();
|
const content = await page.getTextContent();
|
||||||
// This is a simple text extraction. For more advanced formatting, more complex logic is needed.
|
// This is a simple text extraction. For more advanced formatting, more complex logic is needed.
|
||||||
const text = content.items.map((item: any) => item.str).join(' ');
|
const text = content.items.map((item: any) => item.str).join(' ');
|
||||||
markdown += text + '\n\n'; // Add double newline for paragraph breaks between pages
|
markdown += text + '\n\n'; // Add double newline for paragraph breaks between pages
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([markdown], { type: 'text/markdown' });
|
|
||||||
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Conversion Error', 'Failed to convert PDF. It may be image-based or corrupted.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([markdown], { type: 'text/markdown' });
|
||||||
|
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Conversion Error',
|
||||||
|
'Failed to convert PDF. It may be image-based or corrupted.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+27
-25
@@ -1,33 +1,35 @@
|
|||||||
|
|
||||||
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(
|
||||||
const zip = new JSZip();
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
).promise;
|
||||||
const page = await pdf.getPage(i);
|
const zip = new JSZip();
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const canvas = document.createElement('canvas');
|
const page = await pdf.getPage(i);
|
||||||
canvas.height = viewport.height;
|
const viewport = page.getViewport({ scale: 2.0 });
|
||||||
canvas.width = viewport.width;
|
const canvas = document.createElement('canvas');
|
||||||
const context = canvas.getContext('2d');
|
canvas.height = viewport.height;
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
canvas.width = viewport.width;
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
const context = canvas.getContext('2d');
|
||||||
zip.file(`page_${i}.png`, blob as Blob);
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
}
|
const blob = await new Promise((resolve) =>
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
canvas.toBlob(resolve, 'image/png')
|
||||||
downloadFile(zipBlob, 'converted_pngs.zip');
|
);
|
||||||
} catch (e) {
|
zip.file(`page_${i}.png`, blob as Blob);
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to convert PDF to PNG.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'converted_pngs.zip');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Failed to convert PDF to PNG.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+39
-26
@@ -3,36 +3,49 @@ 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(
|
||||||
const zip = new JSZip();
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
|
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);
|
||||||
const viewport = page.getViewport({ scale: 2.0 }); // Use 2x scale for high quality
|
const viewport = page.getViewport({ scale: 2.0 }); // Use 2x scale for high quality
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
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({
|
||||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
canvasContext: context,
|
||||||
const rgba = imageData.data;
|
viewport: viewport,
|
||||||
const tiffBuffer = UTIF.encodeImage(new Uint8Array(rgba), canvas.width, canvas.height);
|
canvas: canvas,
|
||||||
|
}).promise;
|
||||||
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const rgba = imageData.data;
|
||||||
|
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" });
|
|
||||||
downloadFile(zipBlob, 'converted_tiff_images.zip');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to convert PDF to TIFF. The file might be corrupted.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'converted_tiff_images.zip');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert PDF to TIFF. The file might be corrupted.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+27
-24
@@ -1,32 +1,35 @@
|
|||||||
|
|
||||||
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 pdfToWebp() {
|
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(
|
||||||
const zip = new JSZip();
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
).promise;
|
||||||
const page = await pdf.getPage(i);
|
const zip = new JSZip();
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const canvas = document.createElement('canvas');
|
const page = await pdf.getPage(i);
|
||||||
canvas.height = viewport.height;
|
const viewport = page.getViewport({ scale: 2.0 });
|
||||||
canvas.width = viewport.width;
|
const canvas = document.createElement('canvas');
|
||||||
const context = canvas.getContext('2d');
|
canvas.height = viewport.height;
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
canvas.width = viewport.width;
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 0.9));
|
const context = canvas.getContext('2d');
|
||||||
zip.file(`page_${i}.webp`, blob as Blob);
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
}
|
const blob = await new Promise((resolve) =>
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
canvas.toBlob(resolve, 'image/webp', 0.9)
|
||||||
downloadFile(zipBlob, 'converted_webp.zip');
|
);
|
||||||
} catch (e) {
|
zip.file(`page_${i}.webp`, blob as Blob);
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to convert PDF to WebP.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'converted_webp.zip');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Failed to convert PDF to WebP.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+18
-19
@@ -3,25 +3,24 @@ 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.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Creating ZIP file...');
|
showLoader('Creating ZIP file...');
|
||||||
try {
|
try {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const fileBuffer = await readFileAsArrayBuffer(file);
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
zip.file(file.name, fileBuffer as ArrayBuffer);
|
zip.file(file.name, fileBuffer as ArrayBuffer);
|
||||||
}
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
downloadFile(zipBlob, 'pdfs.zip');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to create ZIP file.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'pdfs.zip');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Failed to create ZIP file.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+31
-26
@@ -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';
|
||||||
@@ -6,30 +5,36 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function pngToPdf() {
|
export async function pngToPdf() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one PNG file.');
|
showAlert('No Files', 'Please select at least one PNG file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Creating PDF from PNGs...');
|
showLoader('Creating PDF from PNGs...');
|
||||||
try {
|
try {
|
||||||
const pdfDoc = await PDFLibDocument.create();
|
const pdfDoc = await PDFLibDocument.create();
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const pngBytes = await readFileAsArrayBuffer(file);
|
const pngBytes = await readFileAsArrayBuffer(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, {
|
page.drawImage(pngImage, {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: pngImage.width,
|
width: pngImage.width,
|
||||||
height: pngImage.height,
|
height: pngImage.height,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_pngs.pdf');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to create PDF from PNG images. Ensure all files are valid PNGs.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_pngs.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to create PDF from PNG images. Ensure all files are valid PNGs.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+244
-159
@@ -6,197 +6,282 @@ import * as pdfjsLib from 'pdfjs-dist';
|
|||||||
import { createIcons, icons } from 'lucide';
|
import { createIcons, icons } from 'lucide';
|
||||||
|
|
||||||
const posterizeState = {
|
const posterizeState = {
|
||||||
pdfJsDoc: null,
|
pdfJsDoc: null,
|
||||||
pageSnapshots: {},
|
pageSnapshots: {},
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function renderPosterizePreview(pageNum: number) {
|
async function renderPosterizePreview(pageNum: number) {
|
||||||
if (!posterizeState.pdfJsDoc) return;
|
if (!posterizeState.pdfJsDoc) return;
|
||||||
|
|
||||||
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(
|
||||||
const context = canvas.getContext('2d');
|
'posterize-preview-canvas'
|
||||||
|
) as HTMLCanvasElement;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
if (posterizeState.pageSnapshots[pageNum]) {
|
if (posterizeState.pageSnapshots[pageNum]) {
|
||||||
canvas.width = posterizeState.pageSnapshots[pageNum].width;
|
canvas.width = posterizeState.pageSnapshots[pageNum].width;
|
||||||
canvas.height = posterizeState.pageSnapshots[pageNum].height;
|
canvas.height = posterizeState.pageSnapshots[pageNum].height;
|
||||||
context.putImageData(posterizeState.pageSnapshots[pageNum], 0, 0);
|
context.putImageData(posterizeState.pageSnapshots[pageNum], 0, 0);
|
||||||
} else {
|
} else {
|
||||||
const page = await posterizeState.pdfJsDoc.getPage(pageNum);
|
const page = await posterizeState.pdfJsDoc.getPage(pageNum);
|
||||||
const viewport = page.getViewport({ scale: 1.5 });
|
const viewport = page.getViewport({ scale: 1.5 });
|
||||||
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();
|
||||||
drawGridOverlay();
|
drawGridOverlay();
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
const context = canvas.getContext('2d');
|
'posterize-preview-canvas'
|
||||||
context.putImageData(posterizeState.pageSnapshots[posterizeState.currentPage], 0, 0);
|
) as HTMLCanvasElement;
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
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;
|
||||||
context.setLineDash([10, 5]);
|
context.setLineDash([10, 5]);
|
||||||
|
|
||||||
const cellWidth = canvas.width / cols;
|
const cellWidth = canvas.width / cols;
|
||||||
const cellHeight = canvas.height / rows;
|
const cellHeight = canvas.height / rows;
|
||||||
|
|
||||||
for (let i = 1; i < cols; i++) {
|
for (let i = 1; i < cols; i++) {
|
||||||
context.beginPath();
|
context.beginPath();
|
||||||
context.moveTo(i * cellWidth, 0);
|
context.moveTo(i * cellWidth, 0);
|
||||||
context.lineTo(i * cellWidth, canvas.height);
|
context.lineTo(i * cellWidth, canvas.height);
|
||||||
context.stroke();
|
context.stroke();
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 1; i < rows; i++) {
|
|
||||||
context.beginPath();
|
|
||||||
context.moveTo(0, i * cellHeight);
|
|
||||||
context.lineTo(canvas.width, i * cellHeight);
|
|
||||||
context.stroke();
|
|
||||||
}
|
|
||||||
context.setLineDash([]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < rows; i++) {
|
||||||
|
context.beginPath();
|
||||||
|
context.moveTo(0, i * cellHeight);
|
||||||
|
context.lineTo(canvas.width, i * cellHeight);
|
||||||
|
context.stroke();
|
||||||
|
}
|
||||||
|
context.setLineDash([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
.getPageCount()
|
||||||
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
.toString();
|
||||||
posterizeState.pageSnapshots = {};
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
posterizeState.currentPage = 1;
|
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes })
|
||||||
|
.promise;
|
||||||
|
posterizeState.pageSnapshots = {};
|
||||||
|
posterizeState.currentPage = 1;
|
||||||
|
|
||||||
document.getElementById('total-preview-pages').textContent = posterizeState.pdfJsDoc.numPages.toString();
|
document.getElementById('total-preview-pages').textContent =
|
||||||
await renderPosterizePreview(1);
|
posterizeState.pdfJsDoc.numPages.toString();
|
||||||
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
|
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
|
||||||
|
|
||||||
const newDoc = await PDFDocument.create();
|
const newDoc = await PDFDocument.create();
|
||||||
const totalPages = posterizeState.pdfJsDoc.numPages;
|
const totalPages = posterizeState.pdfJsDoc.numPages;
|
||||||
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 tempCtx = tempCanvas.getContext('2d');
|
|
||||||
|
|
||||||
for (const pageIndex of pageIndicesToProcess) {
|
|
||||||
const page = await posterizeState.pdfJsDoc.getPage(Number(pageIndex) + 1);
|
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
|
||||||
tempCanvas.width = viewport.width;
|
|
||||||
tempCanvas.height = viewport.height;
|
|
||||||
await page.render({ canvasContext: tempCtx, viewport }).promise;
|
|
||||||
|
|
||||||
let [targetWidth, targetHeight] = PageSizes[pageSizeKey];
|
|
||||||
let currentOrientation = orientation;
|
|
||||||
|
|
||||||
if (currentOrientation === 'auto') {
|
|
||||||
currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
|
||||||
} else if (currentOrientation === 'portrait' && targetWidth > targetHeight) {
|
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
|
||||||
}
|
|
||||||
|
|
||||||
const tileWidth = tempCanvas.width / cols;
|
|
||||||
const tileHeight = tempCanvas.height / rows;
|
|
||||||
|
|
||||||
for (let r = 0; r < rows; r++) {
|
|
||||||
for (let c = 0; c < cols; c++) {
|
|
||||||
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
|
||||||
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
|
||||||
const sWidth = 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');
|
|
||||||
tileCanvas.width = sWidth;
|
|
||||||
tileCanvas.height = sHeight;
|
|
||||||
tileCanvas.getContext('2d').drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
|
||||||
|
|
||||||
const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png'));
|
|
||||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
|
||||||
|
|
||||||
const scaleX = newPage.getWidth() / sWidth;
|
|
||||||
const scaleY = newPage.getHeight() / sHeight;
|
|
||||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
|
||||||
|
|
||||||
const scaledWidth = sWidth * scale;
|
|
||||||
const scaledHeight = sHeight * scale;
|
|
||||||
|
|
||||||
newPage.drawImage(tileImage, {
|
|
||||||
x: (newPage.getWidth() - scaledWidth) / 2,
|
|
||||||
y: (newPage.getHeight() - scaledHeight) / 2,
|
|
||||||
width: scaledWidth,
|
|
||||||
height: scaledHeight,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'posterized.pdf');
|
|
||||||
showAlert('Success', 'Your PDF has been posterized.');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', e.message || 'Could not posterize the PDF.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tempCanvas = document.createElement('canvas');
|
||||||
|
const tempCtx = tempCanvas.getContext('2d');
|
||||||
|
|
||||||
|
for (const pageIndex of pageIndicesToProcess) {
|
||||||
|
const page = await posterizeState.pdfJsDoc.getPage(Number(pageIndex) + 1);
|
||||||
|
const viewport = page.getViewport({ scale: 2.0 });
|
||||||
|
tempCanvas.width = viewport.width;
|
||||||
|
tempCanvas.height = viewport.height;
|
||||||
|
await page.render({ canvasContext: tempCtx, viewport }).promise;
|
||||||
|
|
||||||
|
let [targetWidth, targetHeight] = PageSizes[pageSizeKey];
|
||||||
|
let currentOrientation = orientation;
|
||||||
|
|
||||||
|
if (currentOrientation === 'auto') {
|
||||||
|
currentOrientation =
|
||||||
|
viewport.width > viewport.height ? 'landscape' : 'portrait';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
||||||
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||||
|
} else if (
|
||||||
|
currentOrientation === 'portrait' &&
|
||||||
|
targetWidth > targetHeight
|
||||||
|
) {
|
||||||
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileWidth = tempCanvas.width / cols;
|
||||||
|
const tileHeight = tempCanvas.height / rows;
|
||||||
|
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
|
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
||||||
|
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
||||||
|
const sWidth =
|
||||||
|
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');
|
||||||
|
tileCanvas.width = sWidth;
|
||||||
|
tileCanvas.height = sHeight;
|
||||||
|
tileCanvas
|
||||||
|
.getContext('2d')
|
||||||
|
.drawImage(
|
||||||
|
tempCanvas,
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
sWidth,
|
||||||
|
sHeight,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
sWidth,
|
||||||
|
sHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
const tileImage = await newDoc.embedPng(
|
||||||
|
tileCanvas.toDataURL('image/png')
|
||||||
|
);
|
||||||
|
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||||
|
|
||||||
|
const scaleX = newPage.getWidth() / sWidth;
|
||||||
|
const scaleY = newPage.getHeight() / sHeight;
|
||||||
|
const scale =
|
||||||
|
scalingMode === 'fit'
|
||||||
|
? Math.min(scaleX, scaleY)
|
||||||
|
: Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
|
const scaledWidth = sWidth * scale;
|
||||||
|
const scaledHeight = sHeight * scale;
|
||||||
|
|
||||||
|
newPage.drawImage(tileImage, {
|
||||||
|
x: (newPage.getWidth() - scaledWidth) / 2,
|
||||||
|
y: (newPage.getHeight() - scaledHeight) / 2,
|
||||||
|
width: scaledWidth,
|
||||||
|
height: scaledHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdfBytes = await newDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'posterized.pdf'
|
||||||
|
);
|
||||||
|
showAlert('Success', 'Your PDF has been posterized.');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', e.message || 'Could not posterize the PDF.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-28
@@ -6,36 +6,39 @@ import { state } from '../state.js';
|
|||||||
const { rgb } = window.PDFLib;
|
const { rgb } = window.PDFLib;
|
||||||
|
|
||||||
export async function redact(redactions: any, canvasScale: any) {
|
export async function redact(redactions: any, canvasScale: any) {
|
||||||
showLoader('Applying redactions...');
|
showLoader('Applying redactions...');
|
||||||
try {
|
try {
|
||||||
const pdfPages = state.pdfDoc.getPages();
|
const pdfPages = state.pdfDoc.getPages();
|
||||||
const conversionScale = 1 / canvasScale;
|
const conversionScale = 1 / canvasScale;
|
||||||
|
|
||||||
redactions.forEach((r: any) => {
|
redactions.forEach((r: any) => {
|
||||||
const page = pdfPages[r.pageIndex];
|
const page = pdfPages[r.pageIndex];
|
||||||
const { height: pageHeight } = page.getSize();
|
const { height: pageHeight } = page.getSize();
|
||||||
|
|
||||||
// Convert canvas coordinates back to PDF coordinates
|
// Convert canvas coordinates back to PDF coordinates
|
||||||
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,
|
||||||
y: pdfY,
|
y: pdfY,
|
||||||
width: pdfWidth,
|
width: pdfWidth,
|
||||||
height: pdfHeight,
|
height: pdfHeight,
|
||||||
color: rgb(0, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const redactedBytes = await state.pdfDoc.save();
|
const redactedBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([redactedBytes], { type: 'application/pdf' }), 'redacted.pdf');
|
downloadFile(
|
||||||
} catch (e) {
|
new Blob([redactedBytes], { type: 'application/pdf' }),
|
||||||
console.error(e);
|
'redacted.pdf'
|
||||||
showAlert('Error', 'Failed to apply redactions.');
|
);
|
||||||
} finally {
|
} catch (e) {
|
||||||
hideLoader();
|
console.error(e);
|
||||||
}
|
showAlert('Error', 'Failed to apply redactions.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,101 +4,123 @@ import { state } from '../state.js';
|
|||||||
import { PDFName } from 'pdf-lib';
|
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');
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
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 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(
|
||||||
if (pageScope === 'all') {
|
'input[name="page-scope"]:checked'
|
||||||
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
if (pageScope === 'all') {
|
||||||
|
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
|
} 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 rangeInput = document.getElementById('page-range-input').value;
|
||||||
|
if (!rangeInput.trim()) throw new Error('Please enter a page range.');
|
||||||
|
const ranges = rangeInput.split(',');
|
||||||
|
for (const range of ranges) {
|
||||||
|
const trimmedRange = range.trim();
|
||||||
|
if (trimmedRange.includes('-')) {
|
||||||
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
|
||||||
} 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 pageNum = Number(trimmedRange);
|
||||||
const rangeInput = document.getElementById('page-range-input').value;
|
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||||
if (!rangeInput.trim()) throw new Error('Please enter a page range.');
|
targetPageIndices.push(pageNum - 1);
|
||||||
const ranges = rangeInput.split(',');
|
|
||||||
for (const range of ranges) {
|
|
||||||
const trimmedRange = range.trim();
|
|
||||||
if (trimmedRange.includes('-')) {
|
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
|
||||||
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
|
|
||||||
} else {
|
|
||||||
const pageNum = Number(trimmedRange);
|
|
||||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
|
||||||
targetPageIndices.push(pageNum - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
targetPageIndices = [...new Set(targetPageIndices)];
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (targetPageIndices.length === 0) throw new Error('No valid pages were selected.');
|
targetPageIndices = [...new Set(targetPageIndices)];
|
||||||
|
|
||||||
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(cb => cb.value)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typesToRemove.size === 0) throw new Error('Please select at least one annotation type to remove.');
|
|
||||||
|
|
||||||
const pages = state.pdfDoc.getPages();
|
|
||||||
|
|
||||||
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();
|
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'annotations-removed.pdf');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', e.message || 'Could not remove annotations. Please check your page range.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetPageIndices.length === 0)
|
||||||
|
throw new Error('No valid pages were selected.');
|
||||||
|
|
||||||
|
const typesToRemove = new Set(
|
||||||
|
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(
|
||||||
|
(cb) => (cb as HTMLInputElement).value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typesToRemove.size === 0)
|
||||||
|
throw new Error('Please select at least one annotation type to remove.');
|
||||||
|
|
||||||
|
const pages = state.pdfDoc.getPages();
|
||||||
|
|
||||||
|
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();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'annotations-removed.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
e.message || 'Could not remove annotations. Please check your page range.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
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,32 +1,33 @@
|
|||||||
|
|
||||||
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';
|
||||||
|
|
||||||
export async function removeMetadata() {
|
export async function removeMetadata() {
|
||||||
showLoader('Removing all metadata...');
|
showLoader('Removing all metadata...');
|
||||||
try {
|
try {
|
||||||
const infoDict = state.pdfDoc.getInfoDict();
|
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('');
|
state.pdfDoc.setTitle('');
|
||||||
state.pdfDoc.setAuthor('');
|
state.pdfDoc.setAuthor('');
|
||||||
state.pdfDoc.setSubject('');
|
state.pdfDoc.setSubject('');
|
||||||
state.pdfDoc.setKeywords([]);
|
state.pdfDoc.setKeywords([]);
|
||||||
state.pdfDoc.setCreator('');
|
state.pdfDoc.setCreator('');
|
||||||
state.pdfDoc.setProducer('');
|
state.pdfDoc.setProducer('');
|
||||||
|
|
||||||
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' }),
|
||||||
} catch (e) {
|
'metadata-removed.pdf'
|
||||||
console.error(e);
|
);
|
||||||
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
} catch (e) {
|
||||||
} finally {
|
console.error(e);
|
||||||
hideLoader();
|
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
||||||
}
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,24 +3,44 @@ 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(
|
||||||
showLoader('Reversing page order...');
|
(file: File) => file.type === 'application/pdf'
|
||||||
try {
|
);
|
||||||
const newPdf = await PDFLibDocument.create();
|
if (!pdfDocs.length) {
|
||||||
const pageCount = state.pdfDoc.getPageCount();
|
showAlert('Error', 'PDF not loaded.');
|
||||||
const reversedIndices = Array.from({ length: pageCount }, (_, i) => pageCount - 1 - i);
|
return;
|
||||||
|
}
|
||||||
|
showLoader('Reversing page order...');
|
||||||
|
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 pageCount = pdfDoc.getPageCount();
|
||||||
|
const reversedIndices = Array.from(
|
||||||
|
{ length: pageCount },
|
||||||
|
(_, i) => pageCount - 1 - i
|
||||||
|
);
|
||||||
|
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices);
|
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'reversed.pdf');
|
const originalName = file.name.replace(/\.pdf$/i, '');
|
||||||
} catch (e) {
|
const fileName = `${originalName}_reversed.pdf`;
|
||||||
console.error(e);
|
zip.file(fileName, newPdfBytes);
|
||||||
showAlert('Error', 'Could not reverse the PDF pages.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'reversed_pdfs.zip');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not reverse the PDF pages.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+24
-21
@@ -5,26 +5,29 @@ import { state } from '../state.js';
|
|||||||
import { degrees } from 'pdf-lib';
|
import { degrees } from 'pdf-lib';
|
||||||
|
|
||||||
export async function rotate() {
|
export async function rotate() {
|
||||||
showLoader('Applying rotations...');
|
showLoader('Applying rotations...');
|
||||||
try {
|
try {
|
||||||
const pages = state.pdfDoc.getPages();
|
const 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
|
||||||
const rotation = parseInt(item.dataset.rotation || '0');
|
const rotation = parseInt(item.dataset.rotation || '0');
|
||||||
if (rotation !== 0) {
|
if (rotation !== 0) {
|
||||||
const currentRotation = pages[pageIndex].getRotation().angle;
|
const currentRotation = pages[pageIndex].getRotation().angle;
|
||||||
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const rotatedPdfBytes = await state.pdfDoc.save();
|
const rotatedPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([rotatedPdfBytes], { type: 'application/pdf' }), 'rotated.pdf');
|
downloadFile(
|
||||||
} catch (e) {
|
new Blob([rotatedPdfBytes], { type: 'application/pdf' }),
|
||||||
console.error(e);
|
'rotated.pdf'
|
||||||
showAlert('Error', 'Could not apply rotations.');
|
);
|
||||||
} finally {
|
} catch (e) {
|
||||||
hideLoader();
|
console.error(e);
|
||||||
}
|
showAlert('Error', 'Could not apply rotations.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+470
-338
@@ -4,413 +4,545 @@ 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,
|
||||||
pageSnapshot: null,
|
context: null,
|
||||||
drawCanvas: null, drawContext: null, isDrawing: false,
|
pageRendering: false,
|
||||||
savedSignatures: [], placedSignatures: [], activeSignature: null,
|
currentPageNum: 1,
|
||||||
interactionMode: 'none',
|
scale: 1.0,
|
||||||
draggedSigId: null,
|
pageSnapshot: null,
|
||||||
dragOffsetX: 0, dragOffsetY: 0,
|
drawCanvas: null,
|
||||||
hoveredSigId: null,
|
drawContext: null,
|
||||||
resizeHandle: null,
|
isDrawing: false,
|
||||||
|
savedSignatures: [],
|
||||||
|
placedSignatures: [],
|
||||||
|
activeSignature: null,
|
||||||
|
interactionMode: 'none',
|
||||||
|
draggedSigId: null,
|
||||||
|
dragOffsetX: 0,
|
||||||
|
dragOffsetY: 0,
|
||||||
|
hoveredSigId: 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);
|
||||||
const viewport = page.getViewport({ scale: signState.scale });
|
const viewport = page.getViewport({ scale: signState.scale });
|
||||||
signState.canvas.height = viewport.height;
|
signState.canvas.height = viewport.height;
|
||||||
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();
|
||||||
|
|
||||||
signState.pageRendering = false;
|
signState.pageRendering = false;
|
||||||
document.getElementById('current-page-display-sign').textContent = num;
|
document.getElementById('current-page-display-sign').textContent = num;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawSignatures() {
|
function drawSignatures() {
|
||||||
if (!signState.pageSnapshot) return;
|
if (!signState.pageSnapshot) return;
|
||||||
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.context.strokeStyle = '#4f46e5';
|
signState.hoveredSigId === sig.id ||
|
||||||
signState.context.setLineDash([6, 3]);
|
signState.draggedSigId === sig.id
|
||||||
signState.context.strokeRect(sig.x, sig.y, sig.width, sig.height);
|
) {
|
||||||
signState.context.setLineDash([]);
|
signState.context.strokeStyle = '#4f46e5';
|
||||||
|
signState.context.setLineDash([6, 3]);
|
||||||
|
signState.context.strokeRect(sig.x, sig.y, sig.width, sig.height);
|
||||||
|
signState.context.setLineDash([]);
|
||||||
|
|
||||||
drawResizeHandles(sig);
|
drawResizeHandles(sig);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function drawResizeHandles(sig: any) {
|
|
||||||
const handleSize = 8;
|
|
||||||
const halfHandle = handleSize / 2;
|
|
||||||
const handles = getResizeHandles(sig);
|
|
||||||
signState.context.fillStyle = '#4f46e5';
|
|
||||||
Object.values(handles).forEach(handle => {
|
|
||||||
signState.context.fillRect(handle.x - halfHandle, handle.y - halfHandle, handleSize, handleSize);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function drawResizeHandles(sig: any) {
|
||||||
async function fitToWidth() {
|
const handleSize = 8;
|
||||||
const page = await signState.pdf.getPage(signState.currentPageNum);
|
const halfHandle = handleSize / 2;
|
||||||
const container = document.getElementById('canvas-container-sign');
|
const handles = getResizeHandles(sig);
|
||||||
signState.scale = container.clientWidth / page.getViewport({ scale: 1.0 }).width;
|
signState.context.fillStyle = '#4f46e5';
|
||||||
renderPage(signState.currentPageNum);
|
Object.values(handles).forEach((handle) => {
|
||||||
|
signState.context.fillRect(
|
||||||
|
handle.x - halfHandle,
|
||||||
|
handle.y - halfHandle,
|
||||||
|
handleSize,
|
||||||
|
handleSize
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fitToWidth() {
|
||||||
|
const page = await signState.pdf.getPage(signState.currentPageNum);
|
||||||
|
const container = document.getElementById('canvas-container-sign');
|
||||||
|
signState.scale =
|
||||||
|
container.clientWidth / page.getViewport({ scale: 1.0 }).width;
|
||||||
|
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');
|
||||||
|
|
||||||
const rect = signState.drawCanvas.getBoundingClientRect();
|
const rect = signState.drawCanvas.getBoundingClientRect();
|
||||||
const dpi = window.devicePixelRatio || 1;
|
const dpi = window.devicePixelRatio || 1;
|
||||||
signState.drawCanvas.width = rect.width * dpi;
|
signState.drawCanvas.width = rect.width * dpi;
|
||||||
signState.drawCanvas.height = rect.height * dpi;
|
signState.drawCanvas.height = rect.height * dpi;
|
||||||
signState.drawContext.scale(dpi, dpi);
|
signState.drawContext.scale(dpi, dpi);
|
||||||
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;
|
|
||||||
// @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 = colorPicker.value;
|
|
||||||
|
|
||||||
const start = (e: any) => {
|
colorPicker.oninput = () =>
|
||||||
signState.isDrawing = true;
|
(signState.drawContext.strokeStyle = (
|
||||||
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
colorPicker as HTMLInputElement
|
||||||
signState.drawContext.beginPath();
|
).value);
|
||||||
signState.drawContext.moveTo(pos.x, pos.y);
|
signState.drawContext.strokeStyle = (colorPicker as HTMLInputElement).value;
|
||||||
};
|
|
||||||
const draw = (e: any) => {
|
|
||||||
if (!signState.isDrawing) return;
|
|
||||||
e.preventDefault();
|
|
||||||
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
|
||||||
signState.drawContext.lineTo(pos.x, pos.y);
|
|
||||||
signState.drawContext.stroke();
|
|
||||||
};
|
|
||||||
const stop = () => signState.isDrawing = false;
|
|
||||||
|
|
||||||
['mousedown', 'touchstart'].forEach(evt => signState.drawCanvas.addEventListener(evt, start, { passive: false }));
|
const start = (e: any) => {
|
||||||
['mousemove', 'touchmove'].forEach(evt => signState.drawCanvas.addEventListener(evt, draw, { passive: false }));
|
signState.isDrawing = true;
|
||||||
['mouseup', 'mouseleave', 'touchend'].forEach(evt => signState.drawCanvas.addEventListener(evt, stop));
|
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
||||||
|
signState.drawContext.beginPath();
|
||||||
|
signState.drawContext.moveTo(pos.x, pos.y);
|
||||||
|
};
|
||||||
|
const draw = (e: any) => {
|
||||||
|
if (!signState.isDrawing) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const pos = getMousePos(signState.drawCanvas, e.touches ? e.touches[0] : e);
|
||||||
|
signState.drawContext.lineTo(pos.x, pos.y);
|
||||||
|
signState.drawContext.stroke();
|
||||||
|
};
|
||||||
|
const stop = () => (signState.isDrawing = false);
|
||||||
|
|
||||||
|
['mousedown', 'touchstart'].forEach((evt) =>
|
||||||
|
signState.drawCanvas.addEventListener(evt, start, { passive: false })
|
||||||
|
);
|
||||||
|
['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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSignatureToSaved(imageDataUrl: any) {
|
function addSignatureToSaved(imageDataUrl: any) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = imageDataUrl;
|
img.src = imageDataUrl;
|
||||||
signState.savedSignatures.push(img);
|
signState.savedSignatures.push(img);
|
||||||
renderSavedSignatures();
|
renderSavedSignatures();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSavedSignatures() {
|
function renderSavedSignatures() {
|
||||||
const container = document.getElementById('saved-signatures-container');
|
const container = document.getElementById('saved-signatures-container');
|
||||||
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 =
|
||||||
img.className = 'h-full w-auto mx-auto';
|
'saved-signature p-1 bg-white rounded-md cursor-pointer border-2 border-transparent hover:border-indigo-500 h-16';
|
||||||
wrapper.appendChild(img);
|
img.className = 'h-full w-auto mx-auto';
|
||||||
container.appendChild(wrapper);
|
wrapper.appendChild(img);
|
||||||
|
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
|
||||||
wrapper.classList.add('selected');
|
.querySelectorAll('.saved-signature')
|
||||||
};
|
.forEach((el) => el.classList.remove('selected'));
|
||||||
});
|
wrapper.classList.add('selected');
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getResizeHandles(sig: any) {
|
function getResizeHandles(sig: any) {
|
||||||
return {
|
return {
|
||||||
'top-left': { x: sig.x, y: sig.y },
|
'top-left': { x: sig.x, y: sig.y },
|
||||||
'top-middle': { x: sig.x + sig.width / 2, y: sig.y },
|
'top-middle': { x: sig.x + sig.width / 2, y: sig.y },
|
||||||
'top-right': { x: sig.x + sig.width, y: sig.y },
|
'top-right': { x: sig.x + sig.width, y: sig.y },
|
||||||
'middle-left': { x: sig.x, y: sig.y + sig.height / 2 },
|
'middle-left': { x: sig.x, y: sig.y + sig.height / 2 },
|
||||||
'middle-right': { x: sig.x + sig.width, y: sig.y + sig.height / 2 },
|
'middle-right': { x: sig.x + sig.width, y: sig.y + sig.height / 2 },
|
||||||
'bottom-left': { x: sig.x, y: sig.y + sig.height },
|
'bottom-left': { x: sig.x, y: sig.y + sig.height },
|
||||||
'bottom-middle':{ x: sig.x + sig.width / 2, y: sig.y + sig.height },
|
'bottom-middle': { x: sig.x + sig.width / 2, y: sig.y + sig.height },
|
||||||
'bottom-right': { x: sig.x + sig.width, y: sig.y + sig.height },
|
'bottom-right': { x: sig.x + sig.width, y: sig.y + sig.height },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 (
|
||||||
return name;
|
Math.abs(pos.x - handlePos.x) < handleSize &&
|
||||||
}
|
Math.abs(pos.y - handlePos.y) < handleSize
|
||||||
|
) {
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupPlacementListeners() {
|
function setupPlacementListeners() {
|
||||||
const canvas = signState.canvas;
|
const canvas = signState.canvas;
|
||||||
const ghost = document.getElementById('signature-ghost');
|
const ghost = document.getElementById('signature-ghost');
|
||||||
|
|
||||||
const mouseMoveHandler = (e: any) => {
|
const mouseMoveHandler = (e: any) => {
|
||||||
if (signState.interactionMode !== 'none') return;
|
if (signState.interactionMode !== 'none') return;
|
||||||
|
|
||||||
if (signState.activeSignature) {
|
if (signState.activeSignature) {
|
||||||
ghost.style.backgroundImage = `url('${signState.activeSignature.image.src}')`;
|
ghost.style.backgroundImage = `url('${signState.activeSignature.image.src}')`;
|
||||||
ghost.style.width = '150px';
|
ghost.style.width = '150px';
|
||||||
ghost.style.height = `${(signState.activeSignature.image.height / signState.activeSignature.image.width) * 150}px`;
|
ghost.style.height = `${(signState.activeSignature.image.height / signState.activeSignature.image.width) * 150}px`;
|
||||||
ghost.style.left = `${e.clientX + 5}px`;
|
ghost.style.left = `${e.clientX + 5}px`;
|
||||||
ghost.style.top = `${e.clientY + 5}px`;
|
ghost.style.top = `${e.clientY + 5}px`;
|
||||||
ghost.classList.remove('hidden');
|
ghost.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = getMousePos(canvas, e);
|
||||||
|
let foundSigId: any = null;
|
||||||
|
let foundHandle = null;
|
||||||
|
|
||||||
|
signState.placedSignatures
|
||||||
|
.filter((s) => s.pageIndex === signState.currentPageNum - 1)
|
||||||
|
.reverse()
|
||||||
|
.forEach((sig) => {
|
||||||
|
if (foundSigId) return;
|
||||||
|
const handle = getHandleAtPos(pos, sig);
|
||||||
|
if (handle) {
|
||||||
|
foundSigId = sig.id;
|
||||||
|
foundHandle = handle;
|
||||||
|
} 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;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const pos = getMousePos(canvas, e);
|
canvas.className = '';
|
||||||
let foundSigId: any = null;
|
if (foundHandle) {
|
||||||
let foundHandle = null;
|
if (['top-left', 'bottom-right'].includes(foundHandle))
|
||||||
|
canvas.classList.add('resize-nwse');
|
||||||
|
else if (['top-right', 'bottom-left'].includes(foundHandle))
|
||||||
|
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) {
|
||||||
|
canvas.classList.add('movable');
|
||||||
|
}
|
||||||
|
|
||||||
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
|
if (signState.hoveredSigId !== foundSigId) {
|
||||||
if (foundSigId) return;
|
signState.hoveredSigId = foundSigId;
|
||||||
const handle = getHandleAtPos(pos, sig);
|
drawSignatures();
|
||||||
if (handle) {
|
}
|
||||||
foundSigId = sig.id;
|
};
|
||||||
foundHandle = handle;
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.className = '';
|
canvas.addEventListener('mousemove', mouseMoveHandler);
|
||||||
if (foundHandle) {
|
document
|
||||||
if (['top-left', 'bottom-right'].includes(foundHandle)) canvas.classList.add('resize-nwse');
|
.getElementById('canvas-container-sign')
|
||||||
else if (['top-right', 'bottom-left'].includes(foundHandle)) canvas.classList.add('resize-nesw');
|
.addEventListener('mouseleave', () => ghost.classList.add('hidden'));
|
||||||
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');
|
const onDragStart = (e: any) => {
|
||||||
} else if (foundSigId) {
|
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||||
canvas.classList.add('movable');
|
let clickedOnSignature = false;
|
||||||
|
|
||||||
|
signState.placedSignatures
|
||||||
|
.filter((s) => s.pageIndex === signState.currentPageNum - 1)
|
||||||
|
.reverse()
|
||||||
|
.forEach((sig) => {
|
||||||
|
if (clickedOnSignature) return;
|
||||||
|
const handle = getHandleAtPos(pos, sig);
|
||||||
|
if (handle) {
|
||||||
|
signState.interactionMode = 'resize';
|
||||||
|
signState.resizeHandle = handle;
|
||||||
|
signState.draggedSigId = sig.id;
|
||||||
|
clickedOnSignature = true;
|
||||||
|
} 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.draggedSigId = sig.id;
|
||||||
|
signState.dragOffsetX = pos.x - sig.x;
|
||||||
|
signState.dragOffsetY = pos.y - sig.y;
|
||||||
|
clickedOnSignature = true;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (signState.hoveredSigId !== foundSigId) {
|
if (clickedOnSignature) {
|
||||||
signState.hoveredSigId = foundSigId;
|
ghost.classList.add('hidden');
|
||||||
drawSignatures();
|
} else if (signState.activeSignature) {
|
||||||
}
|
const sigWidth = 150;
|
||||||
};
|
const sigHeight =
|
||||||
|
(signState.activeSignature.image.height /
|
||||||
|
signState.activeSignature.image.width) *
|
||||||
|
sigWidth;
|
||||||
|
signState.placedSignatures.push({
|
||||||
|
id: Date.now(),
|
||||||
|
image: signState.activeSignature.image,
|
||||||
|
x: pos.x - sigWidth / 2,
|
||||||
|
y: pos.y - sigHeight / 2,
|
||||||
|
width: sigWidth,
|
||||||
|
height: sigHeight,
|
||||||
|
pageIndex: signState.currentPageNum - 1,
|
||||||
|
aspectRatio: sigWidth / sigHeight,
|
||||||
|
});
|
||||||
|
drawSignatures();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', mouseMoveHandler);
|
const onDragMove = (e: any) => {
|
||||||
document.getElementById('canvas-container-sign').addEventListener('mouseleave', () => ghost.classList.add('hidden'));
|
if (signState.interactionMode === 'none') return;
|
||||||
|
e.preventDefault();
|
||||||
|
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
||||||
|
const sig = signState.placedSignatures.find(
|
||||||
|
(s) => s.id === signState.draggedSigId
|
||||||
|
);
|
||||||
|
if (!sig) return;
|
||||||
|
|
||||||
const onDragStart = (e: any) => {
|
if (signState.interactionMode === 'drag') {
|
||||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
sig.x = pos.x - signState.dragOffsetX;
|
||||||
let clickedOnSignature = false;
|
sig.y = pos.y - signState.dragOffsetY;
|
||||||
|
} else if (signState.interactionMode === 'resize') {
|
||||||
|
const originalRight = sig.x + sig.width;
|
||||||
|
const originalBottom = sig.y + sig.height;
|
||||||
|
|
||||||
signState.placedSignatures.filter(s => s.pageIndex === signState.currentPageNum - 1).reverse().forEach(sig => {
|
if (signState.resizeHandle.includes('right'))
|
||||||
if (clickedOnSignature) return;
|
sig.width = Math.max(20, pos.x - sig.x);
|
||||||
const handle = getHandleAtPos(pos, sig);
|
if (signState.resizeHandle.includes('bottom'))
|
||||||
if (handle) {
|
sig.height = Math.max(20, pos.y - sig.y);
|
||||||
signState.interactionMode = 'resize';
|
if (signState.resizeHandle.includes('left')) {
|
||||||
signState.resizeHandle = handle;
|
sig.width = Math.max(20, originalRight - pos.x);
|
||||||
signState.draggedSigId = sig.id;
|
sig.x = originalRight - sig.width;
|
||||||
clickedOnSignature = true;
|
}
|
||||||
} else if (pos.x >= sig.x && pos.x <= sig.x + sig.width && pos.y >= sig.y && pos.y <= sig.y + sig.height) {
|
if (signState.resizeHandle.includes('top')) {
|
||||||
signState.interactionMode = 'drag';
|
sig.height = Math.max(20, originalBottom - pos.y);
|
||||||
signState.draggedSigId = sig.id;
|
sig.y = originalBottom - sig.height;
|
||||||
signState.dragOffsetX = pos.x - sig.x;
|
}
|
||||||
signState.dragOffsetY = pos.y - sig.y;
|
|
||||||
clickedOnSignature = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (clickedOnSignature) {
|
if (
|
||||||
ghost.classList.add('hidden');
|
signState.resizeHandle.includes('left') ||
|
||||||
} else if (signState.activeSignature) {
|
signState.resizeHandle.includes('right')
|
||||||
const sigWidth = 150;
|
) {
|
||||||
const sigHeight = (signState.activeSignature.image.height / signState.activeSignature.image.width) * sigWidth;
|
sig.height = sig.width / sig.aspectRatio;
|
||||||
signState.placedSignatures.push({
|
} else if (
|
||||||
id: Date.now(), image: signState.activeSignature.image,
|
signState.resizeHandle.includes('top') ||
|
||||||
x: pos.x - sigWidth / 2, y: pos.y - sigHeight / 2,
|
signState.resizeHandle.includes('bottom')
|
||||||
width: sigWidth, height: sigHeight, pageIndex: signState.currentPageNum - 1,
|
) {
|
||||||
aspectRatio: sigWidth / sigHeight,
|
sig.width = sig.height * sig.aspectRatio;
|
||||||
});
|
}
|
||||||
drawSignatures();
|
}
|
||||||
}
|
drawSignatures();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDragMove = (e: any) => {
|
const onDragEnd = () => {
|
||||||
if (signState.interactionMode === 'none') return;
|
signState.interactionMode = 'none';
|
||||||
e.preventDefault();
|
signState.draggedSigId = null;
|
||||||
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
|
drawSignatures();
|
||||||
const sig = signState.placedSignatures.find(s => s.id === signState.draggedSigId);
|
};
|
||||||
if (!sig) return;
|
|
||||||
|
|
||||||
if (signState.interactionMode === 'drag') {
|
['mousedown', 'touchstart'].forEach((evt) =>
|
||||||
sig.x = pos.x - signState.dragOffsetX;
|
canvas.addEventListener(evt, onDragStart, { passive: false })
|
||||||
sig.y = pos.y - signState.dragOffsetY;
|
);
|
||||||
} else if (signState.interactionMode === 'resize') {
|
['mousemove', 'touchmove'].forEach((evt) =>
|
||||||
const originalRight = sig.x + sig.width;
|
canvas.addEventListener(evt, onDragMove, { passive: false })
|
||||||
const originalBottom = sig.y + sig.height;
|
);
|
||||||
|
['mouseup', 'mouseleave', 'touchend'].forEach((evt) =>
|
||||||
if (signState.resizeHandle.includes('right')) sig.width = Math.max(20, pos.x - sig.x);
|
canvas.addEventListener(evt, onDragEnd)
|
||||||
if (signState.resizeHandle.includes('bottom')) sig.height = Math.max(20, pos.y - sig.y);
|
);
|
||||||
if (signState.resizeHandle.includes('left')) {
|
|
||||||
sig.width = Math.max(20, originalRight - pos.x);
|
|
||||||
sig.x = originalRight - sig.width;
|
|
||||||
}
|
|
||||||
if (signState.resizeHandle.includes('top')) {
|
|
||||||
sig.height = Math.max(20, originalBottom - pos.y);
|
|
||||||
sig.y = originalBottom - sig.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signState.resizeHandle.includes('left') || signState.resizeHandle.includes('right')) {
|
|
||||||
sig.height = sig.width / sig.aspectRatio;
|
|
||||||
} else if (signState.resizeHandle.includes('top') || signState.resizeHandle.includes('bottom')) {
|
|
||||||
sig.width = sig.height * sig.aspectRatio;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drawSignatures();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDragEnd = () => {
|
|
||||||
signState.interactionMode = 'none';
|
|
||||||
signState.draggedSigId = null;
|
|
||||||
drawSignatures();
|
|
||||||
};
|
|
||||||
|
|
||||||
['mousedown', 'touchstart'].forEach(evt => canvas.addEventListener(evt, onDragStart, { passive: false }));
|
|
||||||
['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() {
|
||||||
document.getElementById('signature-editor').classList.remove('hidden');
|
document.getElementById('signature-editor').classList.remove('hidden');
|
||||||
|
|
||||||
signState.canvas = document.getElementById('canvas-sign');
|
signState.canvas = document.getElementById('canvas-sign');
|
||||||
signState.context = signState.canvas.getContext('2d');
|
signState.context = signState.canvas.getContext('2d');
|
||||||
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('fit-width-btn').onclick = fitToWidth;
|
}
|
||||||
document.getElementById('undo-btn').onclick = () => { signState.placedSignatures.pop(); drawSignatures(); };
|
};
|
||||||
|
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('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');
|
||||||
});
|
});
|
||||||
tabPanels[index].classList.remove('hidden');
|
tabPanels[index].classList.remove('hidden');
|
||||||
button.classList.add('border-indigo-500', 'text-white');
|
button.classList.add('border-indigo-500', 'text-white');
|
||||||
button.classList.remove('border-transparent', 'text-gray-400');
|
button.classList.remove('border-transparent', 'text-gray-400');
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('clear-draw-btn').onclick = () =>
|
||||||
|
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 fontPreview = document.getElementById('font-preview');
|
||||||
|
const fontFamilySelect = document.getElementById('font-family-select');
|
||||||
|
const fontSizeSlider = document.getElementById('font-size-slider');
|
||||||
|
const fontSizeValue = document.getElementById('font-size-value');
|
||||||
|
const fontColorPicker = document.getElementById('font-color-picker');
|
||||||
|
|
||||||
|
const updateFontPreview = () => {
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
fontPreview.textContent = textInput.value || 'Your Name';
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
fontPreview.style.fontFamily = fontFamilySelect.value;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
fontPreview.style.fontSize = `${fontSizeSlider.value}px`;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
fontPreview.style.color = fontColorPicker.value;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
fontSizeValue.textContent = fontSizeSlider.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
[textInput, fontFamilySelect, fontSizeSlider, fontColorPicker].forEach((el) =>
|
||||||
|
el.addEventListener('input', updateFontPreview)
|
||||||
|
);
|
||||||
|
updateFontPreview();
|
||||||
|
|
||||||
|
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
|
||||||
|
if (!textInput.value) return;
|
||||||
|
const canvas = await html2canvas(fontPreview, {
|
||||||
|
backgroundColor: null,
|
||||||
|
scale: 2,
|
||||||
});
|
});
|
||||||
|
addSignatureToSaved(canvas.toDataURL());
|
||||||
|
};
|
||||||
|
|
||||||
document.getElementById('clear-draw-btn').onclick = () => signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height);
|
document.getElementById('signature-upload-input').onchange = (e) => {
|
||||||
document.getElementById('save-draw-btn').onclick = () => { addSignatureToSaved(signState.drawCanvas.toDataURL()); signState.drawContext.clearRect(0, 0, signState.drawCanvas.width, signState.drawCanvas.height); };
|
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||||
|
const file = e.target.files[0];
|
||||||
const textInput = document.getElementById('signature-text-input');
|
if (!file) return;
|
||||||
const fontPreview = document.getElementById('font-preview');
|
const reader = new FileReader();
|
||||||
const fontFamilySelect = document.getElementById('font-family-select');
|
reader.onload = (event) => addSignatureToSaved(event.target.result);
|
||||||
const fontSizeSlider = document.getElementById('font-size-slider');
|
reader.readAsDataURL(file);
|
||||||
const fontSizeValue = document.getElementById('font-size-value');
|
};
|
||||||
const fontColorPicker = document.getElementById('font-color-picker');
|
|
||||||
|
|
||||||
const updateFontPreview = () => {
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
fontPreview.textContent = textInput.value || 'Your Name';
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
fontPreview.style.fontFamily = fontFamilySelect.value;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
fontPreview.style.fontSize = `${fontSizeSlider.value}px`;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
fontPreview.style.color = fontColorPicker.value;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
fontSizeValue.textContent = fontSizeSlider.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
[textInput, fontFamilySelect, fontSizeSlider, fontColorPicker].forEach(el => el.addEventListener('input', updateFontPreview));
|
|
||||||
updateFontPreview();
|
|
||||||
|
|
||||||
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
|
|
||||||
if (!textInput.value) return;
|
|
||||||
const canvas = await html2canvas(fontPreview, { backgroundColor: null, scale: 2 });
|
|
||||||
addSignatureToSaved(canvas.toDataURL());
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('signature-upload-input').onchange = (e) => {
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (event) => addSignatureToSaved(event.target.result);
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyAndSaveSignatures() {
|
export async function applyAndSaveSignatures() {
|
||||||
if (signState.placedSignatures.length === 0) {
|
if (signState.placedSignatures.length === 0) {
|
||||||
showAlert('No Signatures Placed', 'Please place at least one signature.');
|
showAlert('No Signatures Placed', 'Please place at least one signature.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
showLoader('Applying signatures...');
|
||||||
|
try {
|
||||||
|
const pages = state.pdfDoc.getPages();
|
||||||
|
for (const sig of signState.placedSignatures) {
|
||||||
|
const page = pages[sig.pageIndex];
|
||||||
|
const originalPageSize = page.getSize();
|
||||||
|
const pngBytes = await fetch(sig.image.src).then((res) =>
|
||||||
|
res.arrayBuffer()
|
||||||
|
);
|
||||||
|
const pngImage = await state.pdfDoc.embedPng(pngBytes);
|
||||||
|
|
||||||
|
const renderedPage = await signState.pdf.getPage(sig.pageIndex + 1);
|
||||||
|
const renderedViewport = renderedPage.getViewport({
|
||||||
|
scale: signState.scale,
|
||||||
|
});
|
||||||
|
const scaleRatio = originalPageSize.width / renderedViewport.width;
|
||||||
|
|
||||||
|
page.drawImage(pngImage, {
|
||||||
|
x: sig.x * scaleRatio,
|
||||||
|
y:
|
||||||
|
originalPageSize.height -
|
||||||
|
sig.y * scaleRatio -
|
||||||
|
sig.height * scaleRatio,
|
||||||
|
width: sig.width * scaleRatio,
|
||||||
|
height: sig.height * scaleRatio,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
showLoader('Applying signatures...');
|
|
||||||
try {
|
|
||||||
const pages = state.pdfDoc.getPages();
|
|
||||||
for (const sig of signState.placedSignatures) {
|
|
||||||
const page = pages[sig.pageIndex];
|
|
||||||
const originalPageSize = page.getSize();
|
|
||||||
const pngBytes = await fetch(sig.image.src).then(res => res.arrayBuffer());
|
|
||||||
const pngImage = await state.pdfDoc.embedPng(pngBytes);
|
|
||||||
|
|
||||||
const renderedPage = await signState.pdf.getPage(sig.pageIndex + 1);
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
const renderedViewport = renderedPage.getViewport({ scale: signState.scale });
|
downloadFile(
|
||||||
const scaleRatio = originalPageSize.width / renderedViewport.width;
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'signed.pdf'
|
||||||
page.drawImage(pngImage, {
|
);
|
||||||
x: sig.x * scaleRatio,
|
} catch (e) {
|
||||||
y: originalPageSize.height - (sig.y * scaleRatio) - (sig.height * scaleRatio),
|
console.error(e);
|
||||||
width: sig.width * scaleRatio,
|
showAlert('Error', 'Failed to apply signatures.');
|
||||||
height: sig.height * scaleRatio,
|
} finally {
|
||||||
});
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'signed.pdf');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to apply signatures.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,49 +5,51 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
|
||||||
|
|
||||||
export async function splitInHalf() {
|
export async function splitInHalf() {
|
||||||
// @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 splitType = document.getElementById('split-type').value;
|
const splitType = document.getElementById('split-type').value;
|
||||||
if (!state.pdfDoc) {
|
if (!state.pdfDoc) {
|
||||||
showAlert('Error', 'No PDF document is loaded.');
|
showAlert('Error', 'No PDF document is loaded.');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
showLoader('Splitting PDF pages...');
|
||||||
|
try {
|
||||||
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
const pages = state.pdfDoc.getPages();
|
||||||
|
|
||||||
|
for (let i = 0; i < pages.length; i++) {
|
||||||
|
const originalPage = pages[i];
|
||||||
|
const { width, height } = originalPage.getSize();
|
||||||
|
const whiteColor = rgb(1, 1, 1); // For masking
|
||||||
|
|
||||||
|
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
|
||||||
|
|
||||||
|
// Copy the page twice for all split types
|
||||||
|
const [page1] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||||
|
const [page2] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
||||||
|
|
||||||
|
switch (splitType) {
|
||||||
|
case 'vertical':
|
||||||
|
page1.setCropBox(0, 0, width / 2, height);
|
||||||
|
page2.setCropBox(width / 2, 0, width / 2, height);
|
||||||
|
break;
|
||||||
|
case 'horizontal':
|
||||||
|
page1.setCropBox(0, height / 2, width, height / 2); // Top half
|
||||||
|
page2.setCropBox(0, 0, width, height / 2); // Bottom half
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
newPdfDoc.addPage(page1);
|
||||||
|
newPdfDoc.addPage(page2);
|
||||||
}
|
}
|
||||||
showLoader('Splitting PDF pages...');
|
|
||||||
try {
|
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
|
||||||
const pages = state.pdfDoc.getPages();
|
|
||||||
|
|
||||||
for (let i = 0; i < pages.length; i++) {
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
const originalPage = pages[i];
|
downloadFile(
|
||||||
const { width, height } = originalPage.getSize();
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
const whiteColor = rgb(1, 1, 1); // For masking
|
'split-half.pdf'
|
||||||
|
);
|
||||||
showLoader(`Processing page ${i + 1} of ${pages.length}...`);
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
// Copy the page twice for all split types
|
showAlert('Error', 'An error occurred while splitting the PDF.');
|
||||||
const [page1] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
} finally {
|
||||||
const [page2] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
|
hideLoader();
|
||||||
|
}
|
||||||
switch (splitType) {
|
|
||||||
case 'vertical':
|
|
||||||
page1.setCropBox(0, 0, width / 2, height);
|
|
||||||
page2.setCropBox(width / 2, 0, width / 2, height);
|
|
||||||
break;
|
|
||||||
case 'horizontal':
|
|
||||||
page1.setCropBox(0, height / 2, width, height / 2); // Top half
|
|
||||||
page2.setCropBox(0, 0, width, height / 2); // Bottom half
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
newPdfDoc.addPage(page1);
|
|
||||||
newPdfDoc.addPage(page2);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'split-half.pdf');
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'An error occurred while splitting the PDF.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+198
-172
@@ -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,205 +8,232 @@ 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;
|
||||||
|
|
||||||
const container = document.getElementById('page-selector-grid');
|
const container = document.getElementById('page-selector-grid');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
visualSelectorRendered = true;
|
visualSelectorRendered = true;
|
||||||
|
|
||||||
container.textContent = '';
|
container.textContent = '';
|
||||||
|
|
||||||
showLoader('Rendering page previews...');
|
showLoader('Rendering page previews...');
|
||||||
try {
|
try {
|
||||||
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'.
|
||||||
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
|
||||||
|
|
||||||
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);
|
||||||
const viewport = page.getViewport({ scale: 0.4 });
|
const viewport = page.getViewport({ scale: 0.4 });
|
||||||
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 =
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
|
||||||
wrapper.dataset.pageIndex = i - 1;
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
|
wrapper.dataset.pageIndex = i - 1;
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = canvas.toDataURL();
|
img.src = canvas.toDataURL();
|
||||||
img.className = 'rounded-md w-full h-auto';
|
img.className = 'rounded-md w-full h-auto';
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.className = 'text-center text-xs mt-1 text-gray-300';
|
p.className = 'text-center text-xs mt-1 text-gray-300';
|
||||||
p.textContent = `Page ${i}`;
|
p.textContent = `Page ${i}`;
|
||||||
wrapper.append(img, p);
|
wrapper.append(img, p);
|
||||||
|
|
||||||
const handleSelection = (e: any) => {
|
const handleSelection = (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const isSelected = wrapper.classList.contains('selected');
|
const isSelected = wrapper.classList.contains('selected');
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
wrapper.classList.remove('selected', 'border-indigo-500');
|
wrapper.classList.remove('selected', 'border-indigo-500');
|
||||||
wrapper.classList.add('border-transparent');
|
wrapper.classList.add('border-transparent');
|
||||||
} else {
|
} else {
|
||||||
wrapper.classList.add('selected', 'border-indigo-500');
|
wrapper.classList.add('selected', 'border-indigo-500');
|
||||||
wrapper.classList.remove('border-transparent');
|
wrapper.classList.remove('border-transparent');
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
wrapper.addEventListener('click', handleSelection);
|
|
||||||
wrapper.addEventListener('touchend', handleSelection);
|
|
||||||
|
|
||||||
wrapper.addEventListener('touchstart', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
container.appendChild(wrapper);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error('Error rendering visual selector:', error);
|
|
||||||
showAlert('Error', 'Failed to render page previews.');
|
|
||||||
// 4. ADDED: Reset the flag on error so the user can try again.
|
|
||||||
visualSelectorRendered = false;
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
wrapper.addEventListener('click', handleSelection);
|
||||||
|
wrapper.addEventListener('touchend', handleSelection);
|
||||||
|
|
||||||
|
wrapper.addEventListener('touchstart', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering visual selector:', error);
|
||||||
|
showAlert('Error', 'Failed to render page previews.');
|
||||||
|
// 4. ADDED: Reset the flag on error so the user can try again.
|
||||||
|
visualSelectorRendered = false;
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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');
|
||||||
const visualPanel = document.getElementById('visual-select-panel');
|
const visualPanel = document.getElementById('visual-select-panel');
|
||||||
const evenOddPanel = document.getElementById('even-odd-panel');
|
const evenOddPanel = document.getElementById('even-odd-panel');
|
||||||
const zipOptionWrapper = document.getElementById('zip-option-wrapper');
|
const zipOptionWrapper = document.getElementById('zip-option-wrapper');
|
||||||
const allPagesPanel = document.getElementById('all-pages-panel');
|
const allPagesPanel = document.getElementById('all-pages-panel');
|
||||||
|
|
||||||
if (!splitModeSelect) return;
|
if (!splitModeSelect) return;
|
||||||
|
|
||||||
splitModeSelect.addEventListener('change', (e) => {
|
splitModeSelect.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
|
||||||
const mode = e.target.value;
|
const mode = e.target.value;
|
||||||
|
|
||||||
if (mode !== 'visual') {
|
if (mode !== 'visual') {
|
||||||
visualSelectorRendered = false;
|
visualSelectorRendered = false;
|
||||||
const container = document.getElementById('page-selector-grid');
|
const container = document.getElementById('page-selector-grid');
|
||||||
if (container) container.innerHTML = '';
|
if (container) container.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
rangePanel.classList.add('hidden');
|
rangePanel.classList.add('hidden');
|
||||||
visualPanel.classList.add('hidden');
|
visualPanel.classList.add('hidden');
|
||||||
evenOddPanel.classList.add('hidden');
|
evenOddPanel.classList.add('hidden');
|
||||||
allPagesPanel.classList.add('hidden');
|
allPagesPanel.classList.add('hidden');
|
||||||
zipOptionWrapper.classList.add('hidden');
|
zipOptionWrapper.classList.add('hidden');
|
||||||
|
|
||||||
if (mode === 'range') {
|
if (mode === 'range') {
|
||||||
rangePanel.classList.remove('hidden');
|
rangePanel.classList.remove('hidden');
|
||||||
zipOptionWrapper.classList.remove('hidden');
|
zipOptionWrapper.classList.remove('hidden');
|
||||||
} else if (mode === 'visual') {
|
} else if (mode === 'visual') {
|
||||||
visualPanel.classList.remove('hidden');
|
visualPanel.classList.remove('hidden');
|
||||||
zipOptionWrapper.classList.remove('hidden');
|
zipOptionWrapper.classList.remove('hidden');
|
||||||
renderVisualSelector();
|
renderVisualSelector();
|
||||||
} else if (mode === 'even-odd') {
|
} else if (mode === 'even-odd') {
|
||||||
evenOddPanel.classList.remove('hidden');
|
evenOddPanel.classList.remove('hidden');
|
||||||
} else if (mode === 'all') {
|
} else if (mode === 'all') {
|
||||||
allPagesPanel.classList.remove('hidden');
|
allPagesPanel.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
let indicesToExtract: any = [];
|
let indicesToExtract: any = [];
|
||||||
|
|
||||||
switch (splitMode) {
|
switch (splitMode) {
|
||||||
case 'range':
|
case 'range':
|
||||||
// @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 pageRangeInput = document.getElementById('page-range').value;
|
const pageRangeInput = document.getElementById('page-range').value;
|
||||||
if (!pageRangeInput) throw new Error('Please enter a page range.');
|
if (!pageRangeInput) throw new Error('Please enter a page range.');
|
||||||
const ranges = pageRangeInput.split(',');
|
const ranges = pageRangeInput.split(',');
|
||||||
for (const range of ranges) {
|
for (const range of ranges) {
|
||||||
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 (
|
||||||
for (let i = start; i <= end; i++) indicesToExtract.push(i - 1);
|
isNaN(start) ||
|
||||||
} else {
|
isNaN(end) ||
|
||||||
const pageNum = Number(trimmedRange);
|
start < 1 ||
|
||||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
end > totalPages ||
|
||||||
indicesToExtract.push(pageNum - 1);
|
start > end
|
||||||
}
|
)
|
||||||
}
|
continue;
|
||||||
break;
|
for (let i = start; i <= end; i++) indicesToExtract.push(i - 1);
|
||||||
|
} else {
|
||||||
case 'even-odd':
|
const pageNum = Number(trimmedRange);
|
||||||
const choiceElement = document.querySelector('input[name="even-odd-choice"]:checked');
|
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
|
||||||
if (!choiceElement) throw new Error('Please select even or odd pages.');
|
indicesToExtract.push(pageNum - 1);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
}
|
||||||
const choice = choiceElement.value;
|
|
||||||
for (let i = 0; i < totalPages; i++) {
|
|
||||||
if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i);
|
|
||||||
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'all':
|
|
||||||
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
|
||||||
break;
|
|
||||||
case 'visual':
|
|
||||||
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
|
|
||||||
.map(el => parseInt(el.dataset.pageIndex));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
const uniqueIndices = [...new Set(indicesToExtract)];
|
case 'even-odd':
|
||||||
if (uniqueIndices.length === 0) {
|
const choiceElement = document.querySelector(
|
||||||
throw new Error('No pages were selected for splitting.');
|
'input[name="even-odd-choice"]:checked'
|
||||||
|
);
|
||||||
|
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'.
|
||||||
|
const choice = choiceElement.value;
|
||||||
|
for (let i = 0; i < totalPages; i++) {
|
||||||
|
if (choice === 'even' && (i + 1) % 2 === 0) indicesToExtract.push(i);
|
||||||
|
if (choice === 'odd' && (i + 1) % 2 !== 0) indicesToExtract.push(i);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
if (splitMode === 'all' || (['range', 'visual'].includes(splitMode) && downloadAsZip)) {
|
case 'all':
|
||||||
showLoader('Creating ZIP file...');
|
indicesToExtract = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
const zip = new JSZip();
|
break;
|
||||||
for (const index of uniqueIndices) {
|
case 'visual':
|
||||||
const newPdf = await PDFLibDocument.create();
|
indicesToExtract = Array.from(
|
||||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
|
document.querySelectorAll('.page-thumbnail-wrapper.selected')
|
||||||
newPdf.addPage(copiedPage);
|
)
|
||||||
const pdfBytes = await newPdf.save();
|
// @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(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
.map((el) => parseInt(el.dataset.pageIndex));
|
||||||
zip.file(`page-${index + 1}.pdf`, pdfBytes);
|
break;
|
||||||
}
|
|
||||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
||||||
downloadFile(zipBlob, 'split-pages.zip');
|
|
||||||
} else {
|
|
||||||
const newPdf = await PDFLibDocument.create();
|
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, uniqueIndices as number[]);
|
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
|
||||||
const pdfBytes = await newPdf.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'split-document.pdf');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (splitMode === 'visual') {
|
|
||||||
visualSelectorRendered = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', e.message || 'Failed to split PDF. Please check your selection.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uniqueIndices = [...new Set(indicesToExtract)];
|
||||||
|
if (uniqueIndices.length === 0) {
|
||||||
|
throw new Error('No pages were selected for splitting.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
splitMode === 'all' ||
|
||||||
|
(['range', 'visual'].includes(splitMode) && downloadAsZip)
|
||||||
|
) {
|
||||||
|
showLoader('Creating ZIP file...');
|
||||||
|
const zip = new JSZip();
|
||||||
|
for (const index of uniqueIndices) {
|
||||||
|
const newPdf = await PDFLibDocument.create();
|
||||||
|
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
|
||||||
|
index as number,
|
||||||
|
]);
|
||||||
|
newPdf.addPage(copiedPage);
|
||||||
|
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
|
||||||
|
zip.file(`page-${index + 1}.pdf`, pdfBytes);
|
||||||
|
}
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'split-pages.zip');
|
||||||
|
} else {
|
||||||
|
const newPdf = await PDFLibDocument.create();
|
||||||
|
const copiedPages = await newPdf.copyPages(
|
||||||
|
state.pdfDoc,
|
||||||
|
uniqueIndices as number[]
|
||||||
|
);
|
||||||
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
|
const pdfBytes = await newPdf.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'split-document.pdf'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (splitMode === 'visual') {
|
||||||
|
visualSelectorRendered = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
e.message || 'Failed to split PDF. Please check your selection.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-42
@@ -5,51 +5,64 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
async function convertImageToPngBytes(file: any) {
|
async function convertImageToPngBytes(file: any) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = img.width;
|
canvas.width = img.width;
|
||||||
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) =>
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
canvas.toBlob(res, 'image/png')
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
);
|
||||||
resolve(pngBytes);
|
// @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();
|
||||||
img.onerror = () => reject(new Error('Failed to load image.'));
|
resolve(pngBytes);
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
};
|
||||||
img.src = e.target.result;
|
img.onerror = () => reject(new Error('Failed to load image.'));
|
||||||
};
|
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
|
||||||
reader.onerror = () => reject(new Error('Failed to read file.'));
|
img.src = e.target.result;
|
||||||
reader.readAsDataURL(file);
|
};
|
||||||
});
|
reader.onerror = () => reject(new Error('Failed to read file.'));
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function svgToPdf() {
|
export async function svgToPdf() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one SVG file.');
|
showAlert('No Files', 'Please select at least one SVG file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Converting SVG to PDF...');
|
showLoader('Converting SVG to PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfDoc = await PDFLibDocument.create();
|
const pdfDoc = await PDFLibDocument.create();
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
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,
|
||||||
const pdfBytes = await pdfDoc.save();
|
y: 0,
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_svgs.pdf');
|
width: pngImage.width,
|
||||||
} catch (e) {
|
height: pngImage.height,
|
||||||
console.error(e);
|
});
|
||||||
showAlert('Error', 'Failed to convert SVG to PDF. One of the files may be invalid.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_svgs.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert SVG to PDF. One of the files may be invalid.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+83
-70
@@ -5,78 +5,91 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
|||||||
import { decode } from 'tiff';
|
import { decode } from 'tiff';
|
||||||
|
|
||||||
export async function tiffToPdf() {
|
export async function tiffToPdf() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one TIFF file.');
|
showAlert('No Files', 'Please select at least one TIFF file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Converting TIFF to PDF...');
|
showLoader('Converting TIFF to PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfDoc = await PDFLibDocument.create();
|
const pdfDoc = await PDFLibDocument.create();
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const tiffBytes = await readFileAsArrayBuffer(file);
|
const tiffBytes = await readFileAsArrayBuffer(file);
|
||||||
const ifds = decode(tiffBytes as any);
|
const ifds = decode(tiffBytes as any);
|
||||||
|
|
||||||
for (const ifd of ifds) {
|
for (const ifd of ifds) {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = ifd.width;
|
canvas.width = ifd.width;
|
||||||
canvas.height = ifd.height;
|
canvas.height = ifd.height;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
throw new Error('Failed to get canvas context');
|
throw new Error('Failed to get canvas context');
|
||||||
}
|
|
||||||
|
|
||||||
const imageData = ctx.createImageData(ifd.width, ifd.height);
|
|
||||||
const pixels = imageData.data;
|
|
||||||
|
|
||||||
// Calculate samples per pixel from data length
|
|
||||||
const totalPixels = ifd.width * ifd.height;
|
|
||||||
const samplesPerPixel = ifd.data.length / totalPixels;
|
|
||||||
|
|
||||||
// Convert TIFF data to RGBA
|
|
||||||
for (let i = 0; i < totalPixels; i++) {
|
|
||||||
const dstIndex = i * 4;
|
|
||||||
|
|
||||||
if (samplesPerPixel === 1) {
|
|
||||||
// Grayscale
|
|
||||||
const gray = ifd.data[i];
|
|
||||||
pixels[dstIndex] = gray;
|
|
||||||
pixels[dstIndex + 1] = gray;
|
|
||||||
pixels[dstIndex + 2] = gray;
|
|
||||||
pixels[dstIndex + 3] = 255;
|
|
||||||
} else if (samplesPerPixel === 3) {
|
|
||||||
// RGB
|
|
||||||
const srcIndex = i * 3;
|
|
||||||
pixels[dstIndex] = ifd.data[srcIndex];
|
|
||||||
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
|
||||||
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
|
||||||
pixels[dstIndex + 3] = 255;
|
|
||||||
} else if (samplesPerPixel === 4) {
|
|
||||||
// RGBA
|
|
||||||
const srcIndex = i * 4;
|
|
||||||
pixels[dstIndex] = ifd.data[srcIndex];
|
|
||||||
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
|
||||||
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
|
||||||
pixels[dstIndex + 3] = ifd.data[srcIndex + 3];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.putImageData(imageData, 0, 0);
|
|
||||||
|
|
||||||
const pngBlob = await new Promise<Blob>((res) => canvas.toBlob(res!, 'image/png'));
|
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
|
||||||
|
|
||||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
|
||||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
|
||||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_tiff.pdf');
|
const imageData = ctx.createImageData(ifd.width, ifd.height);
|
||||||
} catch (e) {
|
const pixels = imageData.data;
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.');
|
// Calculate samples per pixel from data length
|
||||||
} finally {
|
const totalPixels = ifd.width * ifd.height;
|
||||||
hideLoader();
|
const samplesPerPixel = ifd.data.length / totalPixels;
|
||||||
|
|
||||||
|
// Convert TIFF data to RGBA
|
||||||
|
for (let i = 0; i < totalPixels; i++) {
|
||||||
|
const dstIndex = i * 4;
|
||||||
|
|
||||||
|
if (samplesPerPixel === 1) {
|
||||||
|
// Grayscale
|
||||||
|
const gray = ifd.data[i];
|
||||||
|
pixels[dstIndex] = gray;
|
||||||
|
pixels[dstIndex + 1] = gray;
|
||||||
|
pixels[dstIndex + 2] = gray;
|
||||||
|
pixels[dstIndex + 3] = 255;
|
||||||
|
} else if (samplesPerPixel === 3) {
|
||||||
|
// RGB
|
||||||
|
const srcIndex = i * 3;
|
||||||
|
pixels[dstIndex] = ifd.data[srcIndex];
|
||||||
|
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
||||||
|
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
||||||
|
pixels[dstIndex + 3] = 255;
|
||||||
|
} else if (samplesPerPixel === 4) {
|
||||||
|
// RGBA
|
||||||
|
const srcIndex = i * 4;
|
||||||
|
pixels[dstIndex] = ifd.data[srcIndex];
|
||||||
|
pixels[dstIndex + 1] = ifd.data[srcIndex + 1];
|
||||||
|
pixels[dstIndex + 2] = ifd.data[srcIndex + 2];
|
||||||
|
pixels[dstIndex + 3] = ifd.data[srcIndex + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
const pngBlob = await new Promise<Blob>((res) =>
|
||||||
|
canvas.toBlob(res!, 'image/png')
|
||||||
|
);
|
||||||
|
const pngBytes = await pngBlob.arrayBuffer();
|
||||||
|
|
||||||
|
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||||
|
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||||
|
page.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: pngImage.width,
|
||||||
|
height: pngImage.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_tiff.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+88
-67
@@ -1,74 +1,95 @@
|
|||||||
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...');
|
||||||
try {
|
try {
|
||||||
// @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('text-input').value;
|
const text = document.getElementById('text-input').value;
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
showAlert('Input Required', 'Please enter some text to convert.');
|
showAlert('Input Required', 'Please enter some text to convert.');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
return;
|
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 fontFamilyKey = document.getElementById('font-family').value;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const pageSizeKey = document.getElementById('page-size').value;
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const colorHex = document.getElementById('text-color').value;
|
|
||||||
const textColor = hexToRgb(colorHex);
|
|
||||||
|
|
||||||
const pdfDoc = await PDFLibDocument.create();
|
|
||||||
const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]);
|
|
||||||
const pageSize = PageSizes[pageSizeKey];
|
|
||||||
const margin = 72; // 1 inch
|
|
||||||
|
|
||||||
let page = pdfDoc.addPage(pageSize);
|
|
||||||
let { width, height } = page.getSize();
|
|
||||||
const textWidth = width - margin * 2;
|
|
||||||
const lineHeight = fontSize * 1.3;
|
|
||||||
let y = height - margin;
|
|
||||||
|
|
||||||
const paragraphs = text.split('\n');
|
|
||||||
for (const paragraph of paragraphs) {
|
|
||||||
const words = paragraph.split(' ');
|
|
||||||
let currentLine = '';
|
|
||||||
for (const word of words) {
|
|
||||||
const testLine = currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
|
||||||
if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) {
|
|
||||||
currentLine = testLine;
|
|
||||||
} else {
|
|
||||||
if (y < margin + lineHeight) {
|
|
||||||
page = pdfDoc.addPage(pageSize);
|
|
||||||
y = page.getHeight() - margin;
|
|
||||||
}
|
|
||||||
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
|
|
||||||
y -= lineHeight;
|
|
||||||
currentLine = word;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentLine.length > 0) {
|
|
||||||
if (y < margin + lineHeight) {
|
|
||||||
page = pdfDoc.addPage(pageSize);
|
|
||||||
y = page.getHeight() - margin;
|
|
||||||
}
|
|
||||||
page.drawText(currentLine, { x: margin, y, font, size: fontSize, color: rgb(textColor.r, textColor.g, textColor.b) });
|
|
||||||
y -= lineHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'text-document.pdf');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to create PDF from text.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const fontFamilyKey = document.getElementById('font-family').value;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const pageSizeKey = document.getElementById('page-size').value;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const colorHex = document.getElementById('text-color').value;
|
||||||
|
const textColor = hexToRgb(colorHex);
|
||||||
|
|
||||||
|
const pdfDoc = await PDFLibDocument.create();
|
||||||
|
const font = await pdfDoc.embedFont(StandardFonts[fontFamilyKey]);
|
||||||
|
const pageSize = PageSizes[pageSizeKey];
|
||||||
|
const margin = 72; // 1 inch
|
||||||
|
|
||||||
|
let page = pdfDoc.addPage(pageSize);
|
||||||
|
let { width, height } = page.getSize();
|
||||||
|
const textWidth = width - margin * 2;
|
||||||
|
const lineHeight = fontSize * 1.3;
|
||||||
|
let y = height - margin;
|
||||||
|
|
||||||
|
const paragraphs = text.split('\n');
|
||||||
|
for (const paragraph of paragraphs) {
|
||||||
|
const words = paragraph.split(' ');
|
||||||
|
let currentLine = '';
|
||||||
|
for (const word of words) {
|
||||||
|
const testLine =
|
||||||
|
currentLine.length > 0 ? `${currentLine} ${word}` : word;
|
||||||
|
if (font.widthOfTextAtSize(testLine, fontSize) <= textWidth) {
|
||||||
|
currentLine = testLine;
|
||||||
|
} else {
|
||||||
|
if (y < margin + lineHeight) {
|
||||||
|
page = pdfDoc.addPage(pageSize);
|
||||||
|
y = page.getHeight() - margin;
|
||||||
|
}
|
||||||
|
page.drawText(currentLine, {
|
||||||
|
x: margin,
|
||||||
|
y,
|
||||||
|
font,
|
||||||
|
size: fontSize,
|
||||||
|
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||||
|
});
|
||||||
|
y -= lineHeight;
|
||||||
|
currentLine = word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentLine.length > 0) {
|
||||||
|
if (y < margin + lineHeight) {
|
||||||
|
page = pdfDoc.addPage(pageSize);
|
||||||
|
y = page.getHeight() - margin;
|
||||||
|
}
|
||||||
|
page.drawText(currentLine, {
|
||||||
|
x: margin,
|
||||||
|
y,
|
||||||
|
font,
|
||||||
|
size: fontSize,
|
||||||
|
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||||
|
});
|
||||||
|
y -= lineHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'text-document.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Failed to create PDF from text.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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('');
|
||||||
}
|
}
|
||||||
+44
-36
@@ -5,44 +5,52 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function webpToPdf() {
|
export async function webpToPdf() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one WebP file.');
|
showAlert('No Files', 'Please select at least one WebP file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showLoader('Converting WebP to PDF...');
|
showLoader('Converting WebP to PDF...');
|
||||||
try {
|
try {
|
||||||
const pdfDoc = await PDFLibDocument.create();
|
const pdfDoc = await PDFLibDocument.create();
|
||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const webpBytes = await readFileAsArrayBuffer(file);
|
const webpBytes = await readFileAsArrayBuffer(file);
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'unknown' is not assignable to type 'BlobPart... Remove this comment to see the full error message
|
// @ts-expect-error TS(2322) FIXME: Type 'unknown' is not assignable to type 'BlobPart... Remove this comment to see the full error message
|
||||||
const imageBitmap = await createImageBitmap(new Blob([webpBytes]));
|
const imageBitmap = await createImageBitmap(new Blob([webpBytes]));
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = imageBitmap.width;
|
canvas.width = imageBitmap.width;
|
||||||
canvas.height = imageBitmap.height;
|
canvas.height = imageBitmap.height;
|
||||||
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) =>
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
canvas.toBlob(resolve, 'image/png')
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
);
|
||||||
|
// @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();
|
||||||
|
|
||||||
// Embed the converted PNG into the PDF
|
// Embed the converted PNG into the PDF
|
||||||
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, {
|
page.drawImage(pngImage, {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: pngImage.width,
|
width: pngImage.width,
|
||||||
height: pngImage.height,
|
height: pngImage.height,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_webp.pdf');
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
showAlert('Error', 'Failed to convert WebP to PDF. Ensure all files are valid WebP images.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
}
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_webp.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert WebP to PDF. Ensure all files are valid WebP images.'
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+106
-97
@@ -4,37 +4,39 @@ import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
export async function wordToPdf() {
|
export async function wordToPdf() {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
showAlert('No File', 'Please upload a .docx file first.');
|
showAlert('No File', 'Please upload a .docx file first.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Preparing preview...');
|
showLoader('Preparing preview...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const mammothOptions = {
|
||||||
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||||
|
convertImage: mammoth.images.inline((element: any) => {
|
||||||
|
return element.read('base64').then((imageBuffer: any) => {
|
||||||
|
return {
|
||||||
|
src: `data:${element.contentType};base64,${imageBuffer}`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
||||||
|
const { value: html } = await mammoth.convertToHtml(
|
||||||
|
{ arrayBuffer },
|
||||||
|
mammothOptions
|
||||||
|
);
|
||||||
|
|
||||||
const mammothOptions = {
|
// Get references to our modal elements from index.html
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
const previewModal = document.getElementById('preview-modal');
|
||||||
convertImage: mammoth.images.inline((element: any) => {
|
const previewContent = document.getElementById('preview-content');
|
||||||
return element.read("base64").then((imageBuffer: any) => {
|
const downloadBtn = document.getElementById('preview-download-btn');
|
||||||
return {
|
const closeBtn = document.getElementById('preview-close-btn');
|
||||||
src: `data:${element.contentType};base64,${imageBuffer}`
|
|
||||||
};
|
|
||||||
});
|
|
||||||
})
|
|
||||||
};
|
|
||||||
const arrayBuffer = await readFileAsArrayBuffer(file);
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
|
|
||||||
const { value: html } = await mammoth.convertToHtml({ arrayBuffer }, mammothOptions);
|
|
||||||
|
|
||||||
// Get references to our modal elements from index.html
|
const styledHtml = `
|
||||||
const previewModal = document.getElementById('preview-modal');
|
|
||||||
const previewContent = document.getElementById('preview-content');
|
|
||||||
const downloadBtn = document.getElementById('preview-download-btn');
|
|
||||||
const closeBtn = document.getElementById('preview-close-btn');
|
|
||||||
|
|
||||||
const styledHtml = `
|
|
||||||
<style>
|
<style>
|
||||||
#preview-content { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: black; }
|
#preview-content { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: black; }
|
||||||
#preview-content table { border-collapse: collapse; width: 100%; }
|
#preview-content table { border-collapse: collapse; width: 100%; }
|
||||||
@@ -44,88 +46,95 @@ export async function wordToPdf() {
|
|||||||
</style>
|
</style>
|
||||||
${html}
|
${html}
|
||||||
`;
|
`;
|
||||||
previewContent.innerHTML = styledHtml;
|
previewContent.innerHTML = styledHtml;
|
||||||
|
|
||||||
const marginDiv = document.createElement('div');
|
const marginDiv = document.createElement('div');
|
||||||
marginDiv.style.height = '100px';
|
marginDiv.style.height = '100px';
|
||||||
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();
|
||||||
else img.onload = resolve;
|
else img.onload = resolve;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await Promise.all(imagePromises);
|
await Promise.all(imagePromises);
|
||||||
|
|
||||||
|
previewModal.classList.remove('hidden');
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
previewModal.classList.remove('hidden');
|
const downloadHandler = async () => {
|
||||||
hideLoader();
|
showLoader('Generating High-Quality PDF...');
|
||||||
|
|
||||||
const downloadHandler = async () => {
|
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||||
showLoader('Generating High-Quality PDF...');
|
const { jsPDF } = window.jspdf;
|
||||||
|
const doc = new jsPDF({
|
||||||
|
orientation: 'p',
|
||||||
|
unit: 'pt',
|
||||||
|
format: 'letter',
|
||||||
|
});
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
await doc.html(previewContent, {
|
||||||
const { jsPDF } = window.jspdf;
|
callback: function (doc: any) {
|
||||||
const doc = new jsPDF({
|
const links = previewContent.querySelectorAll('a');
|
||||||
orientation: 'p',
|
const pageHeight = doc.internal.pageSize.getHeight();
|
||||||
unit: 'pt',
|
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
|
||||||
format: 'letter'
|
|
||||||
});
|
|
||||||
|
|
||||||
await doc.html(previewContent, {
|
links.forEach((link) => {
|
||||||
callback: function (doc: any) {
|
if (!link.href) return;
|
||||||
const links = previewContent.querySelectorAll('a');
|
|
||||||
const pageHeight = doc.internal.pageSize.getHeight();
|
|
||||||
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
|
|
||||||
|
|
||||||
links.forEach(link => {
|
const linkRect = link.getBoundingClientRect();
|
||||||
if (!link.href) return;
|
|
||||||
|
|
||||||
const linkRect = link.getBoundingClientRect();
|
// Calculate position relative to the preview container's top-left
|
||||||
|
const relativeX = linkRect.left - containerRect.left;
|
||||||
|
const relativeY = linkRect.top - containerRect.top;
|
||||||
|
|
||||||
// Calculate position relative to the preview container's top-left
|
const pageNum = Math.floor(relativeY / pageHeight) + 1;
|
||||||
const relativeX = linkRect.left - containerRect.left;
|
const yOnPage = relativeY % pageHeight;
|
||||||
const relativeY = linkRect.top - containerRect.top;
|
|
||||||
|
|
||||||
const pageNum = Math.floor(relativeY / pageHeight) + 1;
|
doc.setPage(pageNum);
|
||||||
const yOnPage = relativeY % pageHeight;
|
try {
|
||||||
|
doc.link(
|
||||||
|
relativeX + 45,
|
||||||
|
yOnPage + 45,
|
||||||
|
linkRect.width,
|
||||||
|
linkRect.height,
|
||||||
|
{ url: link.href }
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not add link:', link.href, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
doc.setPage(pageNum);
|
const outputFileName = `${file.name.replace(/\.[^/.]+$/, '')}.pdf`;
|
||||||
try {
|
doc.save(outputFileName);
|
||||||
doc.link(relativeX + 45, yOnPage + 45, linkRect.width, linkRect.height, { url: link.href });
|
hideLoader();
|
||||||
} catch (e) {
|
},
|
||||||
console.warn("Could not add link:", link.href, e);
|
autoPaging: 'slice',
|
||||||
}
|
x: 45,
|
||||||
});
|
y: 45,
|
||||||
|
width: 522,
|
||||||
|
windowWidth: previewContent.scrollWidth,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const outputFileName = `${file.name.replace(/\.[^/.]+$/, "")}.pdf`;
|
const closeHandler = () => {
|
||||||
doc.save(outputFileName);
|
previewModal.classList.add('hidden');
|
||||||
hideLoader();
|
previewContent.innerHTML = '';
|
||||||
},
|
downloadBtn.removeEventListener('click', downloadHandler);
|
||||||
autoPaging: 'slice',
|
closeBtn.removeEventListener('click', closeHandler);
|
||||||
x: 45,
|
};
|
||||||
y: 45,
|
|
||||||
width: 522,
|
|
||||||
windowWidth: previewContent.scrollWidth
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeHandler = () => {
|
downloadBtn.addEventListener('click', downloadHandler);
|
||||||
previewModal.classList.add('hidden');
|
closeBtn.addEventListener('click', closeHandler);
|
||||||
previewContent.innerHTML = '';
|
} catch (e) {
|
||||||
downloadBtn.removeEventListener('click', downloadHandler);
|
console.error(e);
|
||||||
closeBtn.removeEventListener('click', closeHandler);
|
hideLoader();
|
||||||
};
|
showAlert(
|
||||||
|
'Preview Error',
|
||||||
downloadBtn.addEventListener('click', downloadHandler);
|
`Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`
|
||||||
closeBtn.addEventListener('click', closeHandler);
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
hideLoader();
|
|
||||||
showAlert('Preview Error', `Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+185
-83
@@ -3,115 +3,217 @@ 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 hideBrandingSections = () => {
|
||||||
|
const nav = document.querySelector('nav');
|
||||||
|
if (nav) {
|
||||||
|
nav.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const heroSection = document.getElementById('hero-section');
|
||||||
|
if (heroSection) {
|
||||||
|
heroSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const featuresSection = document.getElementById('features-section');
|
||||||
|
if (featuresSection) {
|
||||||
|
featuresSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const securitySection = document.getElementById(
|
||||||
|
'security-compliance-section'
|
||||||
|
);
|
||||||
|
if (securitySection) {
|
||||||
|
securitySection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const faqSection = document.getElementById('faq-accordion');
|
||||||
|
if (faqSection) {
|
||||||
|
faqSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const testimonialsSection = document.getElementById('testimonials-section');
|
||||||
|
if (testimonialsSection) {
|
||||||
|
testimonialsSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportSection = document.getElementById('support-section');
|
||||||
|
if (supportSection) {
|
||||||
|
supportSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const footer = document.querySelector('footer');
|
||||||
|
if (footer) {
|
||||||
|
footer.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionDividers = document.querySelectorAll('.section-divider');
|
||||||
|
sectionDividers.forEach((divider) => {
|
||||||
|
(divider as HTMLElement).style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.title = 'PDF Tools';
|
||||||
|
|
||||||
|
const toolsHeader = document.getElementById('tools-header');
|
||||||
|
if (toolsHeader) {
|
||||||
|
const title = toolsHeader.querySelector('h2');
|
||||||
|
const subtitle = toolsHeader.querySelector('p');
|
||||||
|
if (title) {
|
||||||
|
title.textContent = 'PDF Tools';
|
||||||
|
title.className = 'text-4xl md:text-5xl font-bold text-white mb-3';
|
||||||
|
}
|
||||||
|
if (subtitle) {
|
||||||
|
subtitle.textContent = 'Select a tool to get started';
|
||||||
|
subtitle.className = 'text-lg text-gray-400';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = document.getElementById('app');
|
||||||
|
if (app) {
|
||||||
|
app.style.paddingTop = '2rem';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const init = () => {
|
const init = () => {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url
|
import.meta.url
|
||||||
).toString();
|
).toString();
|
||||||
|
|
||||||
dom.toolGrid.textContent = '';
|
// Handle simple mode - hide branding sections
|
||||||
|
if (__SIMPLE_MODE__) {
|
||||||
|
hideBrandingSections();
|
||||||
|
}
|
||||||
|
|
||||||
categories.forEach(category => {
|
dom.toolGrid.textContent = '';
|
||||||
const categoryGroup = document.createElement('div');
|
|
||||||
categoryGroup.className = 'category-group col-span-full';
|
|
||||||
|
|
||||||
const title = document.createElement('h2');
|
categories.forEach((category) => {
|
||||||
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0';
|
const categoryGroup = document.createElement('div');
|
||||||
title.textContent = category.name;
|
categoryGroup.className = 'category-group col-span-full';
|
||||||
|
|
||||||
const toolsContainer = document.createElement('div');
|
const title = document.createElement('h2');
|
||||||
toolsContainer.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
|
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0';
|
||||||
|
title.textContent = category.name;
|
||||||
|
|
||||||
category.tools.forEach(tool => {
|
const toolsContainer = document.createElement('div');
|
||||||
const toolCard = document.createElement('div');
|
toolsContainer.className =
|
||||||
toolCard.className = 'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center';
|
'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
|
||||||
toolCard.dataset.toolId = tool.id;
|
|
||||||
|
|
||||||
const icon = document.createElement('i');
|
category.tools.forEach((tool) => {
|
||||||
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
|
const toolCard = document.createElement('div');
|
||||||
icon.setAttribute('data-lucide', tool.icon);
|
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;
|
||||||
|
|
||||||
const toolName = document.createElement('h3');
|
const icon = document.createElement('i');
|
||||||
toolName.className = 'font-semibold text-white';
|
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
|
||||||
toolName.textContent = tool.name;
|
icon.setAttribute('data-lucide', tool.icon);
|
||||||
|
|
||||||
toolCard.append(icon, toolName);
|
const toolName = document.createElement('h3');
|
||||||
|
toolName.className = 'font-semibold text-white';
|
||||||
|
toolName.textContent = tool.name;
|
||||||
|
|
||||||
if (tool.subtitle) {
|
toolCard.append(icon, toolName);
|
||||||
const toolSubtitle = document.createElement('p');
|
|
||||||
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
|
|
||||||
toolSubtitle.textContent = tool.subtitle;
|
|
||||||
toolCard.appendChild(toolSubtitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
toolsContainer.appendChild(toolCard);
|
if (tool.subtitle) {
|
||||||
});
|
const toolSubtitle = document.createElement('p');
|
||||||
|
toolSubtitle.className = 'text-xs text-gray-400 mt-1 px-2';
|
||||||
|
toolSubtitle.textContent = tool.subtitle;
|
||||||
|
toolCard.appendChild(toolSubtitle);
|
||||||
|
}
|
||||||
|
|
||||||
categoryGroup.append(title, toolsContainer);
|
toolsContainer.appendChild(toolCard);
|
||||||
dom.toolGrid.appendChild(categoryGroup);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchBar = document.getElementById('search-bar');
|
categoryGroup.append(title, toolsContainer);
|
||||||
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
|
dom.toolGrid.appendChild(categoryGroup);
|
||||||
|
});
|
||||||
|
|
||||||
searchBar.addEventListener('input', () => {
|
const searchBar = document.getElementById('search-bar');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
|
||||||
const searchTerm = searchBar.value.toLowerCase().trim();
|
|
||||||
|
|
||||||
categoryGroups.forEach(group => {
|
searchBar.addEventListener('input', () => {
|
||||||
const toolCards = group.querySelectorAll('.tool-card');
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
let visibleToolsInCategory = 0;
|
const searchTerm = searchBar.value.toLowerCase().trim();
|
||||||
|
|
||||||
toolCards.forEach(card => {
|
categoryGroups.forEach((group) => {
|
||||||
const toolName = card.querySelector('h3').textContent.toLowerCase();
|
const toolCards = group.querySelectorAll('.tool-card');
|
||||||
const toolSubtitle = card.querySelector('p')?.textContent.toLowerCase() || '';
|
let visibleToolsInCategory = 0;
|
||||||
const isMatch = toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
|
|
||||||
|
|
||||||
card.classList.toggle('hidden', !isMatch);
|
toolCards.forEach((card) => {
|
||||||
if (isMatch) {
|
const toolName = card.querySelector('h3').textContent.toLowerCase();
|
||||||
visibleToolsInCategory++;
|
const toolSubtitle =
|
||||||
}
|
card.querySelector('p')?.textContent.toLowerCase() || '';
|
||||||
});
|
const isMatch =
|
||||||
|
toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
|
||||||
|
|
||||||
group.classList.toggle('hidden', visibleToolsInCategory === 0);
|
card.classList.toggle('hidden', !isMatch);
|
||||||
});
|
if (isMatch) {
|
||||||
});
|
visibleToolsInCategory++;
|
||||||
|
|
||||||
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
|
|
||||||
const card = e.target.closest('.tool-card');
|
|
||||||
if (card) {
|
|
||||||
const toolId = card.dataset.toolId;
|
|
||||||
setupToolInterface(toolId);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group.classList.toggle('hidden', visibleToolsInCategory === 0);
|
||||||
});
|
});
|
||||||
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
|
});
|
||||||
dom.alertOkBtn.addEventListener('click', hideAlert);
|
|
||||||
|
|
||||||
const faqAccordion = document.getElementById('faq-accordion');
|
window.addEventListener('keydown', function (e) {
|
||||||
if (faqAccordion) {
|
const key = e.key.toLowerCase();
|
||||||
faqAccordion.addEventListener('click', (e) => {
|
const isMac = navigator.userAgent.toUpperCase().includes('MAC');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'closest' does not exist on type 'EventTa... Remove this comment to see the full error message
|
const isCtrlK = e.ctrlKey && key === 'k';
|
||||||
const questionButton = e.target.closest('.faq-question');
|
const isCmdK = isMac && e.metaKey && key === 'k';
|
||||||
if (!questionButton) return;
|
|
||||||
|
|
||||||
const faqItem = questionButton.parentElement;
|
if (isCtrlK || isCmdK) {
|
||||||
const answer = faqItem.querySelector('.faq-answer');
|
e.preventDefault();
|
||||||
|
searchBar.focus();
|
||||||
faqItem.classList.toggle('open');
|
|
||||||
|
|
||||||
if (faqItem.classList.contains('open')) {
|
|
||||||
answer.style.maxHeight = answer.scrollHeight + 'px';
|
|
||||||
} else {
|
|
||||||
answer.style.maxHeight = '0px';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
createIcons({ icons });
|
const shortcutK = document.getElementById('shortcut');
|
||||||
console.log('Please share our tool and share the love!');
|
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) => {
|
||||||
|
// @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');
|
||||||
|
if (card) {
|
||||||
|
const toolId = card.dataset.toolId;
|
||||||
|
setupToolInterface(toolId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
|
||||||
|
dom.alertOkBtn.addEventListener('click', hideAlert);
|
||||||
|
|
||||||
|
const faqAccordion = document.getElementById('faq-accordion');
|
||||||
|
if (faqAccordion) {
|
||||||
|
faqAccordion.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
|
||||||
|
const questionButton = e.target.closest('.faq-question');
|
||||||
|
if (!questionButton) return;
|
||||||
|
|
||||||
|
const faqItem = questionButton.parentElement;
|
||||||
|
const answer = faqItem.querySelector('.faq-answer');
|
||||||
|
|
||||||
|
faqItem.classList.toggle('open');
|
||||||
|
|
||||||
|
if (faqItem.classList.contains('open')) {
|
||||||
|
answer.style.maxHeight = answer.scrollHeight + 'px';
|
||||||
|
} else {
|
||||||
|
answer.style.maxHeight = '0px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createIcons({ icons });
|
||||||
|
console.log('Please share our tool and share the love!');
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
+11
-11
@@ -1,17 +1,17 @@
|
|||||||
export const state = {
|
export const state = {
|
||||||
activeTool: null,
|
activeTool: null,
|
||||||
files: [],
|
files: [],
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
pdfPages: [],
|
pdfPages: [],
|
||||||
currentPdfUrl: null,
|
currentPdfUrl: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resets the state when switching views or completing an operation.
|
// Resets the state when switching views or completing an operation.
|
||||||
export function resetState() {
|
export function resetState() {
|
||||||
state.activeTool = null;
|
state.activeTool = null;
|
||||||
state.files = [];
|
state.files = [];
|
||||||
state.pdfDoc = null;
|
state.pdfDoc = null;
|
||||||
state.pdfPages = [];
|
state.pdfPages = [];
|
||||||
state.currentPdfUrl = null;
|
state.currentPdfUrl = null;
|
||||||
document.getElementById('tool-content').innerHTML = '';
|
document.getElementById('tool-content').innerHTML = '';
|
||||||
}
|
}
|
||||||
+364
-248
File diff suppressed because it is too large
Load Diff
+96
-85
@@ -1,114 +1,125 @@
|
|||||||
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 &&
|
||||||
return name;
|
Math.abs(height - size.height) < tolerance) ||
|
||||||
}
|
(Math.abs(width - size.height) < tolerance &&
|
||||||
|
Math.abs(height - size.width) < tolerance)
|
||||||
|
) {
|
||||||
|
return name;
|
||||||
}
|
}
|
||||||
return 'Custom';
|
}
|
||||||
|
return 'Custom';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertPoints(points: any, unit: any) {
|
export function convertPoints(points: any, unit: any) {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'in':
|
case 'in':
|
||||||
result = points / 72;
|
result = points / 72;
|
||||||
break;
|
break;
|
||||||
case 'mm':
|
case 'mm':
|
||||||
result = (points / 72) * 25.4;
|
result = (points / 72) * 25.4;
|
||||||
break;
|
break;
|
||||||
case 'px':
|
case 'px':
|
||||||
result = points * (96 / 72); // Assuming 96 DPI
|
result = points * (96 / 72); // Assuming 96 DPI
|
||||||
break;
|
break;
|
||||||
default: // 'pt'
|
default: // 'pt'
|
||||||
result = points;
|
result = points;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return result.toFixed(2);
|
return result.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
||||||
if (bytes === 0) return '0 Bytes';
|
if (bytes === 0) return '0 Bytes';
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const dm = decimals < 0 ? 0 : decimals;
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const downloadFile = (blob: any, filename: any) => {
|
export const downloadFile = (blob: any, filename: any) => {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = filename;
|
a.download = filename;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readFileAsArrayBuffer = (file: any) => {
|
export const readFileAsArrayBuffer = (file: any) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => resolve(reader.result);
|
reader.onload = () => resolve(reader.result);
|
||||||
reader.onerror = (error) => reject(error);
|
reader.onerror = (error) => reject(error);
|
||||||
reader.readAsArrayBuffer(file);
|
reader.readAsArrayBuffer(file);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parsePageRanges(rangeString: any, totalPages: any) {
|
export function parsePageRanges(rangeString: any, totalPages: any) {
|
||||||
if (!rangeString || rangeString.trim() === '') {
|
if (!rangeString || rangeString.trim() === '') {
|
||||||
return Array.from({ length: totalPages }, (_, i) => i);
|
return Array.from({ length: totalPages }, (_, i) => i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const indices = new Set();
|
||||||
|
const parts = rangeString.split(',');
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const trimmedPart = part.trim();
|
||||||
|
if (!trimmedPart) continue;
|
||||||
|
|
||||||
|
if (trimmedPart.includes('-')) {
|
||||||
|
const [start, end] = trimmedPart.split('-').map(Number);
|
||||||
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
) {
|
||||||
|
console.warn(`Invalid range skipped: ${trimmedPart}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
indices.add(i - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pageNum = Number(trimmedPart);
|
||||||
|
|
||||||
|
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
|
||||||
|
console.warn(`Invalid page number skipped: ${trimmedPart}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
indices.add(pageNum - 1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const indices = new Set();
|
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
||||||
const parts = rangeString.split(',');
|
return Array.from(indices).sort((a, b) => a - b);
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
const trimmedPart = part.trim();
|
|
||||||
if (!trimmedPart) continue;
|
|
||||||
|
|
||||||
if (trimmedPart.includes('-')) {
|
|
||||||
const [start, end] = trimmedPart.split('-').map(Number);
|
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) {
|
|
||||||
console.warn(`Invalid range skipped: ${trimmedPart}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = start; i <= end; i++) {
|
|
||||||
indices.add(i - 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const pageNum = Number(trimmedPart);
|
|
||||||
|
|
||||||
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
|
|
||||||
console.warn(`Invalid page number skipped: ${trimmedPart}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
indices.add(pageNum - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
|
|
||||||
return Array.from(indices).sort((a, b) => a - b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { singlePdfLoadTools, simpleTools, multiFileTools } from '@/js/config/pdf-tools';
|
import {
|
||||||
|
singlePdfLoadTools,
|
||||||
|
simpleTools,
|
||||||
|
multiFileTools,
|
||||||
|
} from '@/js/config/pdf-tools';
|
||||||
|
|
||||||
describe('Tool Configuration Arrays', () => {
|
describe('Tool Configuration Arrays', () => {
|
||||||
|
|
||||||
// --- Tests for singlePdfLoadTools ---
|
// --- Tests for singlePdfLoadTools ---
|
||||||
describe('singlePdfLoadTools', () => {
|
describe('singlePdfLoadTools', () => {
|
||||||
it('should be an array of non-empty strings', () => {
|
it('should be an array of non-empty strings', () => {
|
||||||
@@ -16,7 +19,7 @@ describe('Tool Configuration Arrays', () => {
|
|||||||
|
|
||||||
it('should have the correct number of tools', () => {
|
it('should have the correct number of tools', () => {
|
||||||
// This acts as a snapshot test to catch unexpected additions/removals.
|
// This acts as a snapshot test to catch unexpected additions/removals.
|
||||||
expect(singlePdfLoadTools).toHaveLength(38);
|
expect(singlePdfLoadTools).toHaveLength(39);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not contain any duplicate tools', () => {
|
it('should not contain any duplicate tools', () => {
|
||||||
@@ -30,7 +33,7 @@ describe('Tool Configuration Arrays', () => {
|
|||||||
it('should be an array of non-empty strings', () => {
|
it('should be an array of non-empty strings', () => {
|
||||||
expect(Array.isArray(simpleTools)).toBe(true);
|
expect(Array.isArray(simpleTools)).toBe(true);
|
||||||
expect(simpleTools.length).toBeGreaterThan(0);
|
expect(simpleTools.length).toBeGreaterThan(0);
|
||||||
simpleTools.forEach(tool => {
|
simpleTools.forEach((tool) => {
|
||||||
expect(typeof tool).toBe('string');
|
expect(typeof tool).toBe('string');
|
||||||
expect(tool.length).toBeGreaterThan(0);
|
expect(tool.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@@ -51,14 +54,14 @@ describe('Tool Configuration Arrays', () => {
|
|||||||
it('should be an array of non-empty strings', () => {
|
it('should be an array of non-empty strings', () => {
|
||||||
expect(Array.isArray(multiFileTools)).toBe(true);
|
expect(Array.isArray(multiFileTools)).toBe(true);
|
||||||
expect(multiFileTools.length).toBeGreaterThan(0);
|
expect(multiFileTools.length).toBeGreaterThan(0);
|
||||||
multiFileTools.forEach(tool => {
|
multiFileTools.forEach((tool) => {
|
||||||
expect(typeof tool).toBe('string');
|
expect(typeof tool).toBe('string');
|
||||||
expect(tool.length).toBeGreaterThan(0);
|
expect(tool.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have the correct number of tools', () => {
|
it('should have the correct number of tools', () => {
|
||||||
expect(multiFileTools).toHaveLength(10);
|
expect(multiFileTools).toHaveLength(13);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not contain any duplicate tools', () => {
|
it('should not contain any duplicate tools', () => {
|
||||||
@@ -82,5 +85,4 @@ describe('Tool Configuration Arrays', () => {
|
|||||||
expect(uniqueTools.size).toBe(allTools.length);
|
expect(uniqueTools.size).toBe(allTools.length);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
setupRemoveBlankPagesTool,
|
||||||
|
removeBlankPages,
|
||||||
|
} from '@/js/logic/remove-blank-pages';
|
||||||
|
import * as ui from '@/js/ui';
|
||||||
|
import * as helpers from '@/js/utils/helpers';
|
||||||
|
import { state } from '@/js/state';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
|
||||||
|
if (typeof ImageData === 'undefined') {
|
||||||
|
global.ImageData = class {
|
||||||
|
data: Uint8ClampedArray;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
colorSpace: string;
|
||||||
|
constructor(
|
||||||
|
data: Uint8ClampedArray,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
options?: { colorSpace: string }
|
||||||
|
) {
|
||||||
|
this.data = data;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.colorSpace = options?.colorSpace || 'srgb';
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockContext: CanvasRenderingContext2D = {
|
||||||
|
getImageData: vi.fn(),
|
||||||
|
drawImage: vi.fn(),
|
||||||
|
putImageData: vi.fn(),
|
||||||
|
} as unknown as CanvasRenderingContext2D;
|
||||||
|
|
||||||
|
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue(mockContext);
|
||||||
|
HTMLCanvasElement.prototype.toDataURL = vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue('data:image/png;base64,mock');
|
||||||
|
|
||||||
|
function createMockPage(isBlank: boolean) {
|
||||||
|
return {
|
||||||
|
getViewport: vi.fn(({ scale }) => ({
|
||||||
|
width: 800 * scale,
|
||||||
|
height: 600 * scale,
|
||||||
|
scale,
|
||||||
|
})),
|
||||||
|
render: vi.fn(() => {
|
||||||
|
// Return ImageData depending on blank/content
|
||||||
|
mockContext.getImageData = vi.fn(() => {
|
||||||
|
const size = 100 * 100 * 4;
|
||||||
|
const data = new Uint8ClampedArray(size);
|
||||||
|
|
||||||
|
if (isBlank) {
|
||||||
|
data.fill(255); // fully white = blank
|
||||||
|
} else {
|
||||||
|
// Mostly black pixels to simulate content
|
||||||
|
data.fill(0);
|
||||||
|
// leave some white pixels so analysis updates text
|
||||||
|
for (let i = 0; i < 400; i += 4) {
|
||||||
|
data[i] = 255;
|
||||||
|
data[i + 1] = 255;
|
||||||
|
data[i + 2] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ImageData(data, 100, 100, { colorSpace: 'srgb' });
|
||||||
|
});
|
||||||
|
return { promise: Promise.resolve() };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- MOCK pdfjs-dist --------------------
|
||||||
|
const mockPdfJsDoc = {
|
||||||
|
numPages: 3,
|
||||||
|
getPage: vi.fn((pageNum: number) => {
|
||||||
|
if (pageNum === 2) return Promise.resolve(createMockPage(true)); // Page 2 blank
|
||||||
|
return Promise.resolve(createMockPage(false)); // Pages 1 & 3 content
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mock('pdfjs-dist', () => ({
|
||||||
|
getDocument: vi.fn(() => ({ promise: Promise.resolve(mockPdfJsDoc) })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// -------------------- 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('Remove Blank Pages Tool', () => {
|
||||||
|
let mockNewDoc: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset state pdfDoc
|
||||||
|
state.pdfDoc = {
|
||||||
|
getPageCount: () => 3,
|
||||||
|
save: vi.fn(),
|
||||||
|
copyPages: vi.fn(),
|
||||||
|
addPage: vi.fn(),
|
||||||
|
} 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([4, 5, 6]))),
|
||||||
|
};
|
||||||
|
vi.mocked(PDFDocument.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 = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// -------------------- TEST CASES --------------------
|
||||||
|
|
||||||
|
it('should remove blank pages successfully', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="analysis-text"></div>
|
||||||
|
<input id="sensitivity-slider" type="range" min="0" max="100" value="95" />
|
||||||
|
<span id="sensitivity-value"></span>
|
||||||
|
<div id="analysis-preview" class="hidden"></div>
|
||||||
|
<div id="removed-pages-thumbnails"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await setupRemoveBlankPagesTool();
|
||||||
|
await removeBlankPages();
|
||||||
|
|
||||||
|
expect(ui.showLoader).toHaveBeenCalledWith('Removing blank pages...');
|
||||||
|
expect(PDFDocument.create).toHaveBeenCalled();
|
||||||
|
expect(mockNewDoc.copyPages).toHaveBeenCalled();
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalled();
|
||||||
|
expect(mockNewDoc.save).toHaveBeenCalled();
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalled();
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle all pages blank gracefully', async () => {
|
||||||
|
mockPdfJsDoc.getPage = vi.fn(() => Promise.resolve(createMockPage(true))); // all pages blank
|
||||||
|
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="analysis-text"></div>
|
||||||
|
<input id="sensitivity-slider" type="range" min="0" max="100" value="95" />
|
||||||
|
<span id="sensitivity-value"></span>
|
||||||
|
<div id="analysis-preview" class="hidden"></div>
|
||||||
|
<div id="removed-pages-thumbnails"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await setupRemoveBlankPagesTool();
|
||||||
|
await removeBlankPages();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'No Content Found',
|
||||||
|
expect.stringContaining('All pages were identified as blank')
|
||||||
|
);
|
||||||
|
expect(helpers.downloadFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle no blank pages gracefully', async () => {
|
||||||
|
mockPdfJsDoc.getPage = vi.fn(() => Promise.resolve(createMockPage(false))); // all pages content
|
||||||
|
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="analysis-text"></div>
|
||||||
|
<input id="sensitivity-slider" type="range" min="0" max="100" value="95" />
|
||||||
|
<span id="sensitivity-value"></span>
|
||||||
|
<div id="analysis-preview" class="hidden"></div>
|
||||||
|
<div id="removed-pages-thumbnails"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await setupRemoveBlankPagesTool();
|
||||||
|
await removeBlankPages();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'No Pages Removed',
|
||||||
|
'No pages were identified as blank at the current sensitivity level.'
|
||||||
|
);
|
||||||
|
expect(helpers.downloadFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
import { reversePages } from '../js/logic/reverse-pages';
|
||||||
|
import { state } from '../js/state';
|
||||||
|
import * as helpers from '../js/utils/helpers';
|
||||||
|
import * as ui from '../js/ui';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
vi.mock('../js/ui', () => ({
|
||||||
|
showLoader: vi.fn(),
|
||||||
|
hideLoader: vi.fn(),
|
||||||
|
showAlert: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../js/utils/helpers', () => ({
|
||||||
|
downloadFile: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('pdf-lib', () => ({
|
||||||
|
PDFDocument: {
|
||||||
|
create: vi.fn(),
|
||||||
|
load: vi.fn().mockResolvedValue({
|
||||||
|
getPageCount: vi.fn(() => 2),
|
||||||
|
copyPages: vi.fn((_, indices) =>
|
||||||
|
Promise.resolve(indices.map((i) => ({ page: `page-${i}` })))
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('reversePages - multi PDF support', () => {
|
||||||
|
let mockNewDoc: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state.files = []; // ✅ now using files, not pdfDocs
|
||||||
|
|
||||||
|
mockNewDoc = {
|
||||||
|
copyPages: vi.fn((doc: any, indices: number[]) =>
|
||||||
|
Promise.resolve(indices.map((i: number) => ({ page: `page-${i}` })))
|
||||||
|
),
|
||||||
|
addPage: vi.fn(),
|
||||||
|
save: vi.fn(() => Promise.resolve(new Uint8Array([1, 2, 3]))),
|
||||||
|
};
|
||||||
|
vi.mocked(PDFLibDocument.create).mockResolvedValue(mockNewDoc);
|
||||||
|
|
||||||
|
vi.mocked(helpers.downloadFile).mockImplementation(() => {});
|
||||||
|
vi.mocked(ui.showLoader).mockImplementation(() => {});
|
||||||
|
vi.mocked(ui.hideLoader).mockImplementation(() => {});
|
||||||
|
vi.mocked(ui.showAlert).mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reverse pages for multiple PDFs and create a zip', async () => {
|
||||||
|
const mockFile = (name: string) => ({
|
||||||
|
name,
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||||
|
});
|
||||||
|
|
||||||
|
state.files = [mockFile('a.pdf'), mockFile('b.pdf')]; // ✅ now matches function
|
||||||
|
|
||||||
|
await reversePages();
|
||||||
|
|
||||||
|
expect(helpers.downloadFile).toHaveBeenCalledWith(
|
||||||
|
expect.any(Blob),
|
||||||
|
'reversed_pdfs.zip'
|
||||||
|
);
|
||||||
|
expect(mockNewDoc.copyPages).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockNewDoc.addPage).toHaveBeenCalled();
|
||||||
|
expect(mockNewDoc.save).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty PDF list gracefully', async () => {
|
||||||
|
state.files = [];
|
||||||
|
await reversePages();
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'PDF not loaded.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle PDF creation errors', async () => {
|
||||||
|
vi.mocked(PDFLibDocument.create).mockRejectedValue(
|
||||||
|
new Error('Create failed')
|
||||||
|
);
|
||||||
|
state.files = [
|
||||||
|
{
|
||||||
|
name: 'x.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await reversePages();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
'Could not reverse the PDF pages.'
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle PDF processing errors', async () => {
|
||||||
|
mockNewDoc.copyPages.mockRejectedValue(new Error('Copy failed'));
|
||||||
|
state.files = [
|
||||||
|
{
|
||||||
|
name: 'y.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await reversePages();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
'Could not reverse the PDF pages.'
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle save errors', async () => {
|
||||||
|
mockNewDoc.save.mockRejectedValue(new Error('Save failed'));
|
||||||
|
state.files = [
|
||||||
|
{
|
||||||
|
name: 'z.pdf',
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await reversePages();
|
||||||
|
|
||||||
|
expect(ui.showAlert).toHaveBeenCalledWith(
|
||||||
|
'Error',
|
||||||
|
'Could not reverse the PDF pages.'
|
||||||
|
);
|
||||||
|
expect(ui.hideLoader).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-1
@@ -13,7 +13,7 @@ global.ResizeObserver = vi.fn().mockImplementation(() => ({
|
|||||||
|
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
value: vi.fn().mockImplementation(query => ({
|
value: vi.fn().mockImplementation((query) => ({
|
||||||
matches: false,
|
matches: false,
|
||||||
media: query,
|
media: query,
|
||||||
onchange: null,
|
onchange: null,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user