Compare commits

..
16 Commits
Author SHA1 Message Date
abdullahalam123 23e414a8be test: update expected tool count in pdf-tools test 2025-10-15 11:55:25 +05:30
abdullahalam123 f682986c29 Merge branch 'main' into posterize 2025-10-15 11:23:48 +05:30
Alam ae18e5c647 Merge pull request #15 from phamleduy04/main
feat: cicd for build and push to docker registry
2025-10-14 22:19:56 +05:30
Duy Pham 412f83950d feat: cicd for build and push to docker registry 2025-10-14 10:29:36 -05:00
abdullahalam123 1424540081 fix: add mjs MIME type to nginx configuration
Ensure proper handling of JavaScript modules with .mjs extension by adding the correct MIME type
2025-10-14 15:40:56 +05:30
abdullahalam123 bbe2283611 feat(posterize): enhance posterize tool with page navigation and auto orientation
- Add page navigation controls to preview different pages
- Implement automatic orientation detection based on page dimensions
- Improve grid overlay rendering to only show for selected pages
- Cache page snapshots for better performance
2025-10-14 14:35:38 +05:30
Alam 6df86037f9 Merge pull request #10 from compliment/posterize
Fix(posterize): Resolve incorrect vertical tiling/cropping
2025-10-14 12:33:33 +05:30
Nishant ed2629e69b Fix(posterize): Resolve incorrect vertical tiling/cropping
The Y-axis calculation in the `posterize` function was backward for `pdf-lib`'s bottom-left origin, causing the resulting PDF tiles to display only the bottom portion of the content, leaving large empty crops.

Corrected the `y` coordinate calculation using `tileRowIndexFromBottom = rows - 1 - r` to properly map the top-to-bottom loop index to the required bottom-up positioning.
2025-10-14 12:20:04 +05:30
abdullahalam123 0c1351adc5 feat(pdf-tools): add posterize tool to split pages into grid
Add new posterize feature that allows splitting PDF pages into a grid of smaller pages. The tool includes:
- Configurable rows and columns
- Page size and orientation options
- Content scaling modes
- Overlap settings for assembly
- Page range selection
2025-10-14 11:40:22 +05:30
Alam ea8b350215 Update README.md
Added kofi link
2025-10-13 16:51:26 +05:30
abdullahalam123 7e9ac95375 fix: update contact email in Code of Conduct and Contributing documents; enhance HTML structure and styling in index.html 2025-10-12 20:26:24 +05:30
abdullahalam123 3f90e5ed8d feat(docker): add docker configuration files and update styles
refactor: convert script tags to link tags for stylesheets in HTML files
fix: update PDF download to use Uint8Array for blob creation
style: reformat CSS file for better readability and organization
2025-10-12 18:23:13 +05:30
abdullahalam123 5e72d7ca2d feat(test): add comprehensive test suite and testing infrastructure
- Add vitest configuration with coverage reporting
- Create test setup file with DOM mocks
- Add tests for state management, helpers, and tool configurations
- Update tsconfig and package.json for testing support
- Clean up unused comments and improve code style
2025-10-12 17:47:08 +05:30
abdullahalam123 3e52545a6c feat: add Code of Conduct, Contribution guidelines, and License files; update HTML files to use module stylesheets 2025-10-12 15:24:29 +05:30
abdullahalam123 47f4818e25 refactor(file-handler): replace innerHTML with DOM methods for safer rendering
refactor(form-filler): improve form field creation with DOM methods
refactor(compare-pdfs): use textContent instead of innerHTML
refactor(duplicate-organize): replace innerHTML with DOM methods
style(main): remove commented start app line
feat(config): add pdf-tools config file with tool categories
2025-10-12 12:51:54 +05:30
abdullahalam123 671297320e feat: add initial project setup with core PDF tools and utilities
- Implement core PDF manipulation tools (split, merge, convert, etc.)
- Add state management and UI utilities
- Set up build configuration with Vite and TailwindCSS
- Include essential dependencies for PDF processing
- Add gitignore and basic project configuration files
2025-10-12 11:55:45 +05:30
97 changed files with 24945 additions and 7 deletions
+10
View File
@@ -0,0 +1,10 @@
node_modules
npm-debug.log
dist
coverage
.git
.gitignore
.env
*.md
.vscode
.idea
+30
View File
@@ -0,0 +1,30 @@
name: build docker image and push to dockerhub
on:
push:
branches:
- "main"
jobs:
docker-build-and-push:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: bentopdf/bentopdf:latest
platforms: linux/amd64,linux/arm64
+28
View File
@@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Coverage reports
coverage/
*.lcov
+65
View File
@@ -0,0 +1,65 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers of **BentoPDF** pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
---
## Our Standards
Examples of behavior that contributes to a positive environment for our community include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy toward other community members
Examples of unacceptable behavior include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others’ private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
---
## Enforcement Responsibilities
Project maintainers of **BentoPDF** are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
---
## Reporting Guidelines
If you are subject to or witness unacceptable behavior, or have any other concerns, please report it by contacting the project team at:
**Email:** `[email protected]`
All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.
---
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing the project include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
---
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at the email above. All complaints will be reviewed and investigated promptly and fairly.
Maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent consequences as determined by other members of the project's leadership.
---
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html), available at https://www.contributor-covenant.org.
+91
View File
@@ -0,0 +1,91 @@
# Contributing to BentoPDF
First off, thank you for considering contributing to **BentoPDF**! Your help makes this project better for everyone.
This document outlines how to contribute, report issues, and get involved in the project.
---
## 1. How to Contribute
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.
- **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.
- **Documentation:** Help improve the README, usage examples, or guides.
- **Testing:** Help test new releases or changes to ensure stability.
---
## 2. Getting Started with Code Contributions
1. **Fork the Repository**
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bento-pdf
npm install
```
2. **Create a New Branch**
```bash
git checkout -b feature/my-new-feature
```
3. **Make Your Changes**
- Follow the code style and conventions used in the project.
- Add comments where necessary.
- Update or add tests if applicable.
4. **Run Tests**
```bash
npm run test
```
5. **Commit Your Changes**
```bash
git add .
git commit -m "Add a meaningful commit message"
```
6. **Push and Submit a Pull Request**
```bash
git push origin feature/my-new-feature
```
- Open a pull request on GitHub and provide a clear description of your changes.
---
## 3. Code Style
- Follow the existing TypeScript and JavaScript conventions.
- Use `camelCase` for variables and functions.
- Keep lines reasonably short and readable.
- Comment complex logic for clarity.
---
## 4. Issues and Pull Requests
- Make sure your PR is focused and addresses a single issue or feature.
- Reference related issues in your PR description (e.g., `Closes #12`).
- Be responsive to feedback and make requested changes promptly.
---
## 5. Reporting Security Issues
If you discover a security vulnerability, please **do not** open a public issue. Instead, contact the project maintainer directly at:
**Email:** [[email protected]](mailto:[email protected])
---
## 6. Code of Conduct
All contributors are expected to follow the Code of Conduct. Be respectful and considerate in all communications.
---
Thank you for helping make **BentoPDF** a better library for everyone!
+22
View File
@@ -0,0 +1,22 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Build without type checking (vite build only)
RUN npm run build -- --mode production
# Production stage
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+161
View File
@@ -0,0 +1,161 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but not
limited to compiled object code, generated documentation, and
conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other
modifications represent, as a whole, an original work of authorship.
"Contribution" shall mean any work of authorship, including the
original version of the Work and any modifications or additions to
that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright
owner or by an individual or Legal Entity authorized to submit on
behalf of the copyright owner.
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity alleging that the
Work or a Contribution incorporated within the Work constitutes
direct or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
shall any Contributor be liable to You for damages arising as a
result of this License or out of the use or inability to use the
Work.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer
acceptance of support, warranty, indemnity, or other liability
obligations, but only on Your own behalf.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format.
Copyright 2025 Abdullah Alam
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+198
View File
@@ -0,0 +1,198 @@
# BentoPDF
![Docker Pulls](https://img.shields.io/docker/pulls/bentopdf/bentopdf)
[![Ko-fi](https://img.shields.io/badge/Buy%20me%20a%20Coffee-yellow?logo=kofi&style=flat-square)](https://ko-fi.com/alio0)
**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.
---
## ✨ Why BentoPDF?
- **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.
- **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.
---
## 🛠️ Features / Tools Supported
BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
### Organize & Manage PDFs
| Tool Name | Description |
| :--- | :--- |
| **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. |
| **Extract Pages** | Save a specific range of pages as a new PDF. |
| **Delete Pages** | Remove unwanted pages from your document. |
| **Rotate PDF** | Rotate individual or all pages in a document. |
| **N-Up PDF** | Combine multiple pages onto a single page. |
| **View PDF** | A powerful, integrated PDF viewer. |
### Edit & Modify PDFs
| Tool Name | Description |
| :--- | :--- |
| **PDF Editor** | A comprehensive editor to modify your PDFs. |
| **Add Page Numbers** | Easily add page numbers with customizable formatting. |
| **Add Watermark** | Add text or image watermarks to protect your documents. |
| **Header & Footer** | Add customizable headers and footers. |
| **Crop PDF** | Crop specific pages or the entire document. |
| **Invert Colors** | Invert the colors of your PDF pages for better readability. |
| **Change Background** | Modify the background color of your PDF. |
| **Change Text Color**| Change the color of text content within the PDF. |
| **Fill Forms** | Fill out PDF forms directly in your browser. |
| **Flatten PDF** | Flatten form fields and annotations into static content. |
| **Remove Annotations**| Remove comments, highlights, and other annotations. |
### Convert to PDF
| 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. |
| **Text to PDF** | Convert plain text files into a PDF. |
### Convert from PDF
| 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. |
| **OCR PDF** | Make scanned PDFs searchable and copyable using Optical Character Recognition. |
### Secure & Optimize PDFs
| Tool Name | Description |
| :--- | :--- |
| **Compress PDF** | Reduce file size while maintaining quality. |
| **Repair PDF** | Attempt to repair and recover data from a corrupted PDF. |
| **Encrypt PDF** | Add a password to protect your PDF from unauthorized access. |
| **Decrypt PDF** | Remove password protection from a PDF (password required). |
| **Change Permissions**| Set or modify user permissions for printing, copying, and editing. |
| **Sign PDF** | Add your digital signature to a document. |
| **Redact Content** | Permanently remove sensitive content from your PDFs. |
| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). |
| **Remove Metadata** | Strip all metadata from your PDF for privacy. |
---
## 🚀 Getting Started
You can run BentoPDF locally for development or personal use.
### 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)
You can run BentoPDF directly from Docker Hub without cloning the repository:
```bash
docker pull bentopdf/bentopdf:latest
docker run -p 3000:80 bentopdf/bentopdf:latest
```
Open your browser at: http://localhost:3000.
This is the fastest way to try BentoPDF without setting up a development environment.
### 🚀 Installation 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`.
### 🚀 Running with Docker Compose Locally
1. **Clone the Repository**:
```bash
git clone https://github.com/alam00000/bentopdf.git
cd bentopdf
```
2. **Run with Docker Compose**:
Create a `docker-compose.yml` file with the content provided in the repository or use the existing one. Then, run:
```bash
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:
```bash
docker-compose up --build -d
```
This ensures your latest changes are applied inside the container
---
## 🛠️ 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:
- **Vite**: A fast build tool for modern web development.
- **TypeScript**: For type safety and an improved developer experience.
- **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.
---
## 🗺️ Roadmap
### 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.
- **Sanitize PDF**: Remove potentially malicious content like scripts from PDFs.
- **Convert to PDF/A**: Convert PDFs to the PDF/A archival format.
- **Edit PDF Content**: Directly edit text and other content within your PDF.
- **Linearize PDF**: Optimize PDFs for fast web viewing.
Contributions and discussions on the roadmap are welcome! Join the conversation via [Discord](https://discord.gg/q42xWQmJ).
---
## 🤝 Contributing
We welcome contributions from the community! Here’s how you can get started:
1. **Fork the repository** and create your branch from `main`.
2. Follow the **Getting Started** steps to set up your local environment.
3. Make your changes and commit them with a clear message.
4. **Open a Pull Request** and describe the changes you've made.
Have an idea for a new tool or an improvement? [Open an issue](https://github.com/alam00000/bentopdf/issues) to discuss it first.
---
## 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:
- **[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.
- **[PDFKit](https://pdfkit.org/)** – For creating and editing PDFs with ease.
- **[EmbedPDF](https://github.com/embedpdf/embed-pdf-viewer)** – For seamless PDF embedding in web pages.
- **[Cropper.js](https://fengyuanchen.github.io/cropperjs/)** – For intuitive image cropping functionality.
- **[Vite](https://vitejs.dev/)** – For lightning-fast development and build tooling.
- **[Tailwind CSS](https://tailwindcss.com/)** – For rapid, flexible, and beautiful UI styling.
Your work inspires and empowers developers everywhere. Thank you for making open-source amazing!
## 📜 License
This project is licensed under the **Apache 2.0**. See the [LICENSE](https://github.com/alam00000/bentopdf/blob/main/LICENSE) file for details.
+199
View File
@@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About Bentopdf - Fast, Private, and Free PDF Tools</title>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="icon" type="image/png" href="images/favicon.svg">
<link href="/src/css/styles.css" rel="stylesheet">
</head>
<body class="antialiased bg-gray-900 text-gray-300">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4">
<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">
<img src="images/favicon.svg" alt="Bentopdf Logo" class="h-8 w-8 mr-2">
<span class="text-white font-bold text-xl">BentoPDF</span>
</a>
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
<div class="md:hidden flex items-center gap-4">
<a href="index.html" class="nav-link">Home</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
</div>
</div>
</nav>
<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>
<div class="section-divider"></div>
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
<div class="text-center">
<i data-lucide="rocket" class="w-16 h-16 text-indigo-400 mx-auto mb-6"></i>
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Our Mission</h2>
<p class="text-lg text-gray-400 leading-relaxed">
To provide the most comprehensive PDF toolbox that respects your privacy and never asks for payment.
We believe essential document tools should be accessible to everyone, everywhere, without barriers.
</p>
</div>
</section>
<div class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700">
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
<div class="text-center md:text-left">
<span class="text-indigo-400 font-bold uppercase">Our Core Philosophy</span>
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">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>
<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>
<footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0">
<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">
<span class="text-xl font-bold text-white">BentoPDF</span>
</div>
<p class="text-gray-400 text-sm">&copy; 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>
</footer>
<script>
lucide.createIcons();
</script>
</body>
</html>
+108
View File
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact Us - BentoPDF</title>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="icon" type="image/png" href="images/favicon.svg">
<link href="/src/css/styles.css" rel="stylesheet">
</head>
<body class="antialiased bg-gray-900 text-gray-300">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16">
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
<span class="text-white font-bold text-xl"> BentoPDF</span>
</a>
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
<div class="md:hidden flex items-center gap-4">
<a href="index.html" class="nav-link">Home</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
</div>
</div>
</nav>
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
<section id="contact-hero" class="text-center py-16 md:py-24">
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">Get in Touch</h1>
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
We'd love to hear from you. Whether you have a question, feedback, or a feature request, please don't
hesitate to reach out.
</p>
</section>
<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>
<footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0">
<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">
<span class="text-xl font-bold text-white">Bentopdf</span>
</div>
<p class="text-gray-400 text-sm">&copy; 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>
</footer>
<script>
lucide.createIcons();
</script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
version: '3.8'
services:
bentopdf:
build:
context: .
dockerfile: Dockerfile
container_name: bentopdf
ports:
- "3000:80"
restart: unless-stopped
-7
View File
@@ -1,7 +0,0 @@
{
"schemaVersion": 1,
"label": "Docker Downloads",
"message": "15.1M",
"color": "blue",
"namedLogo": "docker"
}
+211
View File
@@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Frequently Asked Questions - BentoPDF</title>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="icon" type="image/png" href="images/favicon.svg">
<link href="/src/css/styles.css" rel="stylesheet">
</head>
<body class="antialiased bg-gray-900 text-gray-300">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4">
<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">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
<span class="text-white font-bold text-xl">BentoPDF</span>
</a>
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
<div class="md:hidden flex items-center gap-4">
<a href="index.html" class="nav-link">Home</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
</div>
</div>
</nav>
<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>
<div class="max-w-4xl mx-auto space-y-4">
<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 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>
</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">Is BentoPDF really free? What's the catch?</h3>
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
</summary>
<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>
<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>
<footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0">
<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">
<span class="text-xl font-bold text-white">Bento PDF</span>
</div>
<p class="text-gray-400 text-sm">&copy; 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>
</footer>
<script>
lucide.createIcons();
</script>
</body>
</html>
+590
View File
@@ -0,0 +1,590 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BentoPDF - The Privacy First PDF Toolkit</title>
<script async src="https://scripts.simpleanalyticscdn.com/latest.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.6.1/cropper.min.css" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Dancing+Script:[email protected]&family=Great+Vibes&family=Kalam:wght@300;400;700&family=Lato:ital,wght@0,400;0,700;1,400&family=Merriweather:ital,wght@0,400;0,700;1,400&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Cedarville+Cursive&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" type="image/png" href="./images/favicon.svg">
<link href="/src/css/styles.css" rel="stylesheet">
</head>
<body class="antialiased">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16">
<div class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8">
<span class="text-white font-bold text-xl ml-2"> <a href="index.html">BentoPDF</a> </span>
</div>
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
<div class="md:hidden flex items-center gap-2">
<a href="index.html" class="nav-link">Home</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
</div>
</div>
<div id="mobile-menu" class="hidden md:hidden">
<div class="px-2 pt-2 pb-3 space-y-1 sm:px-3">
<a href="./about.html" class="mobile-nav-link">About</a>
<a href="./contact.html" class="mobile-nav-link">Contact</a>
<hr class="border-gray-700 my-2">
<a href="#tool-grid" id="mobile-all-tools-link" class="mobile-nav-link bg-gray-700 text-white">View All
Tools</a>
</div>
</div>
</nav>
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
<section id="hero-section" class="text-center py-20">
<h1 class="text-4xl md:text-7xl font-bold text-white mb-4">
The <span class="marker-slanted">
PDF Toolkit
</span> built for privacy<span
class="text-4xl md:text-6xl text-transparent bg-clip-text bg-gradient-to-r from-indigo-500 to-purple-500">.</span>
</h1>
<p class="text-lg text-gray-400 mb-8">
Fast, Secure and Forever Free.
</p>
<div class="flex flex-wrap justify-center items-center gap-2 sm:gap-4 mb-8">
<span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
<i data-lucide="check-circle" class="w-4 h-4"></i>
No Signups
</span>
<span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
<i data-lucide="check-circle" class="w-4 h-4"></i>
Unlimited Use
</span>
<span
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-gray-800/50 border border-indigo-500/30 text-indigo-300 text-sm font-medium backdrop-blur-sm">
<i data-lucide="check-circle" class="w-4 h-4"></i>
Works Offline
</span>
</div>
<div class="flex flex-col items-center gap-4">
<a href="#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">Start
Using - Forever Free</a>
</div>
</section>
<div class="section-divider"></div>
<section id="features-section" class="py-20">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
Why <span class="marker-slanted">BentoPDF?</span>
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">No Signup</h3>
</div>
<p class="text-gray-400 pl-14">Start instantly, no accounts or emails.</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="shield" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">No Uploads</h3>
</div>
<p class="text-gray-400 pl-14">100% client-side, your files never leave your device.</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">Forever Free</h3>
</div>
<p class="text-gray-400 pl-14">All tools, no trials, no paywalls.</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="infinity" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">No Limits</h3>
</div>
<p class="text-gray-400 pl-14">Use as much as you want, no hidden caps.</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="layers" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">Batch Processing</h3>
</div>
<p class="text-gray-400 pl-14">Handle unlimited PDFs in one go.</p>
</div>
<div class="bg-gray-800 p-6 rounded-lg">
<div class="flex items-center gap-4 mb-3">
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0"></i>
<h3 class="text-xl font-bold text-white">Lightning Fast</h3>
</div>
<p class="text-gray-400 pl-14">Process PDFs instantly, without waiting or delays.</p>
</div>
</div>
</section>
<div class="section-divider"></div>
<div id="tools-header" class="text-center mb-12">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-3">
Get Started with <span class="marker-slanted ml-2"> Tools</span>
</h2>
<p class="text-gray-400">Click a tool to open the file uploader</p>
</div>
<div id="grid-view">
<div class="mb-8 max-w-lg mx-auto">
<div class="relative">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
<i data-lucide="search" class="w-5 h-5 text-gray-400"></i>
</span>
<input type="text" id="search-bar"
class="w-full pl-10 pr-4 py-3 bg-gray-700 text-white border border-gray-600 rounded-lg focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Search for a tool (e.g., 'split', 'organize'...)">
</div>
</div>
<div id="tool-grid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6">
</div>
</div>
<div id="tool-interface"
class="hidden w-full max-w-4xl mx-auto bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8 border border-gray-700">
<button id="back-to-grid"
class="flex items-center gap-2 text-indigo-400 hover:text-indigo-300 mb-6 font-semibold">
<i data-lucide="arrow-left" class="cursor-pointer"></i>
<span class="cursor-pointer"> Back to Tools </span>
</button>
<div id="tool-content">
</div>
</div>
<div id="loader-modal"
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-gray-800 p-8 rounded-lg flex flex-col items-center gap-4 border border-gray-700 shadow-xl">
<div class="solid-spinner"></div>
<p id="loader-text" class="text-white text-lg font-medium">Processing...</p>
</div>
</div>
<div id="alert-modal"
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 p-4">
<div class="bg-gray-800 max-w-sm w-full p-6 rounded-lg border border-gray-700 shadow-xl text-center">
<h3 id="alert-title" class="text-xl font-bold text-white mb-2">Alert</h3>
<p id="alert-message" class="text-gray-300 mb-6">This is an alert message.</p>
<button id="alert-ok"
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">OK</button>
</div>
</div>
<div id="preview-modal"
class="hidden fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-40 p-4">
<div class="bg-white text-black rounded-lg shadow-xl w-full max-w-4xl h-[90vh] flex flex-col">
<div class="p-4 border-b flex justify-between items-center">
<h3 class="text-xl font-bold">Document Preview</h3>
<div class="flex gap-4">
<button id="preview-download-btn"
class="btn bg-indigo-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-indigo-700">Download
as PDF</button>
<button id="preview-close-btn"
class="btn bg-gray-600 text-white font-semibold py-2 px-6 rounded-lg hover:bg-gray-700">Close</button>
</div>
</div>
<div id="preview-content" class="p-8 overflow-y-auto flex-grow">
</div>
</div>
</div>
<div class="section-divider hide-section mb-20 mt-10"></div>
<!-- COMPLIANCE SECTION START -->
<section id="security-compliance-section" class="py-20 hide-section">
<div class="mb-8 text-center">
<h2 class="text-3xl md:text-4xl lg:text-5xl font-bold text-white leading-tight mb-6 text-balance">
Your data never leaves your device
<span class="inline-flex items-center gap-2">
<i data-lucide="shield"
class="w-8 h-8 md:w-10 md:h-10 text-indigo-400 bg-indigo-900 rounded-lg p-1.5"></i>
We keep
</span>
<br class="hidden sm:block" />
<span
class="inline-block border-2 border-indigo-400 bg-indigo-900 text-indigo-300 px-4 py-2 rounded-full mx-2 text-2xl md:text-3xl lg:text-4xl font-bold">
your information safe
</span>
by following global security standards.
</h2>
</div>
<div class="mb-16 text-center">
<span class="inline-flex items-center gap-2 text-indigo-400 text-lg font-medium transition-colors">
All the processing happens locally on your device.
</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-4">
<!-- GDPR Compliance -->
<div class="flex flex-col items-center text-center">
<div
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
<img src="https://mkt-cf.pdffiller.com/mrk/350/images/_global/logos/security-badges/logo-gdpr.svg"
alt="GDPR compliance" class="w-full h-full">
</div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3">GDPR compliance</h3>
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">Protects the personal data
and privacy of individuals within the European Union.</p>
</div>
<div class="flex flex-col items-center text-center">
<div
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
<img src="https://mkt-cf.pdffiller.com/mrk/350/images/_global/logos/security-badges/logo-ccpa.svg"
alt="CCPA compliance" class="w-full h-full">
</div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3">CCPA compliance</h3>
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">Gives California residents
rights over how their personal information is collected, used, and shared.</p>
</div>
<div class="flex flex-col items-center text-center">
<div
class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-blue-600 flex items-center justify-center mb-4">
<img src="https://mkt-cf.pdffiller.com/mrk/350/images/_global/logos/security-badges/logo-hipaa.svg"
alt="HIPAA compliance" class="w-full h-full">
</div>
<h3 class="text-lg md:text-xl font-bold text-white mb-3">HIPAA compliance</h3>
<p class="text-gray-400 text-sm md:text-base leading-relaxed max-w-xs">Sets safeguards for handling
sensitive health information in the United States healthcare system.</p>
</div>
</div>
</section>
<!-- COMPLAINCE SECTION END -->
<div class="section-divider hide-section"></div>
<section id="faq-accordion" class="space-y-4 hide-section">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12 mt-8">
Frequently Asked <span class="marker-slanted">Questions</span>
</h2>
<!-- Existing FAQs here... -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Is BentoPDF really free?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
Yes, absolutely. All tools on BentoPDF are 100% free to use, with no file limits, no sign-ups,
and no
watermarks. We believe everyone deserves access to simple, powerful PDF tools without a paywall.
</p>
</div>
</div>
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Are my files secure? Where are they processed?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
Your files are as secure as possible because they **never leave your computer**. All processing
happens
directly in your web browser (client-side). We never upload your files to a server, so you
maintain
complete privacy and control over your documents.
</p>
</div>
</div>
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Does it work on Mac, Windows, and Mobile?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
Yes! Since BentoPDF runs entirely in your browser, it works on any operating system with a
modern web
browser, including Windows, macOS, Linux, iOS, and Android.
</p>
</div>
</div>
<!-- New FAQ: GDPR Compliance -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Is BentoPDF GDPR compliant?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
Yes. BentoPDF is fully GDPR compliant. Since all file processing happens locally in your browser
and we
never collect or transmit your files to any server, we have no access to your data. This ensures
you are
always in control of your documents.
</p>
</div>
</div>
<!-- New FAQ: Data Storage -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Do you store or track any of my files?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
No. We never store, track, or log your files. Everything you do on BentoPDF happens in your
browser
memory and disappears once you close the page. There are no uploads, no history logs, and no
servers
involved.
</p>
</div>
</div>
<!-- New FAQ: Privacy -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">What makes BentoPDF different from other PDF
tools?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
Most PDF tools upload your files to a server for processing. BentoPDF never does that. We use
secure,
modern web technology to process your files directly in your browser. This means faster
performance,
stronger privacy, and complete peace of mind.
</p>
</div>
</div>
<!-- New FAQ: Browser-Based -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">How does browser-based processing keep me
safe?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
By running entirely inside your browser, BentoPDF ensures that your files never leave your
device. This
eliminates the risks of server hacks, data breaches, or unauthorized access. Your files remain
yours—always.
</p>
</div>
</div>
<!-- New FAQ: Analytics -->
<div class="faq-item bg-gray-800 rounded-lg border border-gray-700">
<button class="faq-question w-full flex justify-between items-center text-left p-6">
<span class="text-lg font-semibold text-white">Do you use cookies or analytics to track me?</span>
<i data-lucide="chevron-down" class="faq-icon w-6 h-6 text-gray-400 transition-transform"></i>
</button>
<div class="faq-answer max-h-0 overflow-hidden transition-all duration-300 ease-in-out">
<p class="p-6 pt-0 text-gray-400">
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>
</div>
</section>
<div class="section-divider hide-section"></div>
<section id="testimonials-section" class="py-20 hide-section">
<h2 class="text-3xl md:text-4xl font-bold text-center text-white mb-12">
What Our <span class="marker-slanted">Users</span> Say
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
<div class="testimonial-card">
<div class="flex items-center mb-4">
<div>
<p class="font-bold text-white">Sarah L.</p>
<div class="text-yellow-400 flex items-center text-sm">★★★★★</div>
</div>
</div>
<p class="text-gray-400">"This is the tool I've been searching for! It's fast, free, and I love that
my confidential documents never get uploaded to some random server. A lifesaver for my freelance
work."</p>
</div>
<div class="testimonial-card">
<div class="flex items-center mb-4">
<div>
<p class="font-bold text-white">Mark Chen</p>
<div class="text-yellow-400 flex items-center text-sm">★★★★★</div>
</div>
</div>
<p class="text-gray-400">"Finally, a PDF editor that just works. No ads, no sign-ups, no nonsense.
The merge tool is surprisingly powerful. I've already bookmarked it on all my devices."</p>
</div>
<div class="testimonial-card">
<div class="flex items-center mb-4">
<div>
<p class="font-bold text-white">Anonymous User A-35Z</p>
<div class="text-yellow-400 flex items-center text-sm">★☆☆☆☆</div>
</div>
</div>
<p class="text-gray-400">"Terrible. It won't let me upload my files to the cloud. How is my Big Data
Tech Overlord supposed to know I signed a permission slip for my kid's field trip? Useless for
my data profile."</p>
</div>
<div class="testimonial-card">
<div class="flex items-center mb-4">
<div>
<p class="font-bold text-white">Dr. Brickson</p>
<div class="text-yellow-400 flex items-center text-sm">★★★★★</div>
</div>
</div>
<p class="text-gray-400">"As a researcher, data privacy is paramount. BentoPDF's client-side
processing model is exactly what my institution recommends. It's robust, reliable, and secure. A
fantastic resource."</p>
</div>
<div class="testimonial-card">
<div class="flex items-center mb-4">
<div>
<p class="font-bold text-white">AdTracker Pro</p>
<div class="text-yellow-400 flex items-center text-sm">★☆☆☆☆</div>
</div>
</div>
<p class="text-gray-400">"This website is broken. My ad blocker says it hasn't blocked a single
tracker. How am I supposed to know if a product is good if it's not following me around the
internet for a week? 1 star."</p>
</div>
<div class="testimonial-card">
<div class="flex items-center mb-4">
<div>
<p class="font-bold text-white">Raj P.</p>
<div class="text-yellow-400 flex items-center text-sm">★★★★★</div>
</div>
</div>
<p class="text-gray-400">"Simple, elegant, and powerful. I needed to merge 50 reports, and it
handled it instantly without crashing my browser. This is what a web tool should be. Highly
recommended."</p>
</div>
</div>
</section>
<div class="section-divider"></div>
<section id="support-section" class="py-20">
<div
class="max-w-4xl mx-auto text-center bg-gray-800 p-8 md:p-12 rounded-xl border border-gray-700 shadow-2xl">
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
Like My Work?
</h2>
<p class="text-gray-400 mb-8 max-w-2xl mx-auto">
BentoPDF is a passion project, built to provide a free, private, and powerful PDF toolkit for
everyone. If you find it useful, consider supporting its development. Every coffee helps!
</p>
<a href="https://ko-fi.com/alio01" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-3 px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold
focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400
hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1 mt-5">
<i data-lucide="coffee" class="w-7 h-7"></i>
<span>Buy Me a Coffee</span>
</a>
</a>
</div>
</section>
<div id="signature-ghost" class="hidden"></div>
</div>
<footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0">
<div class="flex items-center justify-center md:justify-start mb-4">
<img src="public/images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3">
<span class="text-xl font-bold text-white">BentoPDF</span>
</div>
<p class="text-gray-400 text-sm">&copy; 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://github.com/alam00000/bentopdf" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="GitHub">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fill-rule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clip-rule="evenodd" />
</svg>
</a>
<a href="https://discord.gg/q42xWQmJ" target="_blank" rel="noopener noreferrer"
class="text-gray-400 hover:text-indigo-400" title="Discord">
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z" />
</svg>
</a>
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400" title="Instagram">
<i data-lucide="instagram"></i>
</a>
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400" title="LinkedIn">
<i data-lucide="linkedin"></i>
</a>
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400" title="X (Twitter)">
<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>
</footer>
<script type="module" src="src/js/main.ts"></script>
</body>
</html>
+36
View File
@@ -0,0 +1,36 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
types {
application/javascript mjs;
}
default_type application/octet-stream;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}
}
+12869
View File
File diff suppressed because it is too large Load Diff
+55
View File
@@ -0,0 +1,55 @@
{
"name": "bento-pdf",
"private": true,
"version": "0.0.0",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"obfuscate": "node scripts/build.mjs",
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest watch",
"build:docker": "vite build"
},
"devDependencies": {
"@testing-library/dom": "^10.4.1",
"@types/blob-stream": "^0.1.33",
"@types/html2canvas": "^0.5.35",
"@types/pdfkit": "^0.17.3",
"@types/sortablejs": "^1.15.8",
"@types/utif": "^3.0.6",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"jsdom": "^27.0.0",
"ts-migrate": "^0.1.35",
"typescript": "~5.9.3",
"vite": "^7.1.7",
"vite-plugin-node-polyfills": "^0.24.0",
"vitest": "^3.2.4"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
"blob-stream": "^0.1.3",
"cropperjs": "^1.6.1",
"heic2any": "^0.0.4",
"html2canvas": "^1.4.1",
"javascript-obfuscator": "^4.1.1",
"jspdf": "^3.0.3",
"jszip": "^3.10.1",
"lucide": "^0.545.0",
"pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.4.296",
"pdfkit": "^0.17.2",
"sortablejs": "^1.15.6",
"tailwindcss": "^4.1.14",
"terser": "^5.44.0",
"tesseract.js": "^6.0.1",
"tiff": "^7.1.2",
"utif": "^3.1.0"
}
}
+166
View File
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy - BentoPDF</title>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="icon" type="image/png" href="images/favicon.svg">
<link href="/src/css/styles.css" rel="stylesheet">
</head>
<body class="antialiased bg-gray-900 text-gray-300">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16">
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
<img src="images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8 mr-2">
<span class="text-white font-bold text-xl">BentoPDF</span>
</a>
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
<div class="md:hidden flex items-center gap-4">
<a href="index.html" class="nav-link">Home</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
</div>
</div>
</nav>
<div id="app" class="container mx-auto p-4 md:p-8">
<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>
<p class="text-center text-gray-500">Last Updated: September 14, 2025</p>
<div class="legal-content mt-12">
<h2>1. Our Commitment to Privacy</h2>
<p>BentoPDF ("we", "us", "our") is fundamentally a privacy-focused service. This Privacy Policy outlines
our unwavering commitment to protecting your privacy. Our core principle is simple: <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>
<p>Unlike other online PDF services, BentoPDF does not upload your files to a server for processing. The
tools you use are powered by 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>
<p>Because of our client-side architecture, we are technically incapable of collecting the following
information:</p>
<ul>
<li>The content of your PDF files or any other documents you use with our tools.</li>
<li>Any personal data contained within your documents.</li>
<li>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>
<p>To improve our website and services, we may collect anonymous, non-personally identifiable
information. This type of data helps us understand how users interact with our site, which tools are
most popular, and how we can improve the user experience. This includes:</p>
<ul>
<li><strong>Usage Analytics:</strong> Anonymized data such as which tools are used, how often they
are used, and which features are accessed. This is aggregated and cannot be tied back to an
individual user or document.</li>
<li><strong>Performance Data:</strong> Anonymized error reports or performance metrics to help us
identify and fix bugs. This data contains no personal information or file content.</li>
</ul>
<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>
<p>BentoPDF is built using powerful, open-source libraries like PDF-lib.js and PDF.js. These libraries
are trusted by developers 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>
<p>Since your files are never transmitted over the internet to our 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>
<p>Our services are not directed at individuals under the age of 13. We 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>
<p>We may update this Privacy Policy from time to time. We will notify 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>
<p>If you have any questions about this Privacy Policy, please contact us at <a
href="mailto:[email protected]">[email protected]</a>.</p>
</div>
</section>
</div>
<footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0">
<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">
<span class="text-xl font-bold text-white">BentoPDF</span>
</div>
<p class="text-gray-400 text-sm">&copy; 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>
</footer>
<script>
lucide.createIcons();
</script>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
<svg width="86" height="166" viewBox="0 0 86 166" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M56.7848 80.9116L0 25.6211L24.8434 1.43151C26.8037 -0.47717 29.9814 -0.47717 31.9414 1.43151L85.177 53.2662L56.7848 80.9116Z" fill="#A5B4FC"/>
<path d="M-1.30805e-05 80.7335L21.2939 101.467L42.5878 80.7335L21.2939 60L-1.30805e-05 80.7335Z" fill="#E0E7FF"/>
<path d="M56.7848 83L81.6279 107.19C83.5881 109.098 83.5881 112.192 81.6279 114.101L28.3925 165.936L0 138.29L56.7848 83Z" fill="#6366F1"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

+56
View File
@@ -0,0 +1,56 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const sourceDir = path.join(__dirname, '../unoptimized-js');
const outputDir = path.join(__dirname, '../dist/js');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
function processFile(filePath) {
const relativePath = path.relative(sourceDir, filePath);
const outputPath = path.join(outputDir, relativePath);
const outputDirForFile = path.dirname(outputPath);
// Create subdirectories in the output folder
if (!fs.existsSync(outputDirForFile)) {
fs.mkdirSync(outputDirForFile, { recursive: true });
}
try {
// 1. Obfuscate the file
console.log(`Obfuscating: ${filePath}`);
const obfuscateCmd = `npx javascript-obfuscator "${filePath}" --output "${outputPath}"`;
execSync(obfuscateCmd);
// 2. Minify the obfuscated file using Terser
console.log(`Minifying: ${outputPath}`);
const minifyCmd = `npx terser "${outputPath}" -o "${outputPath}" --compress --mangle`;
execSync(minifyCmd);
console.log(`Success: ${outputPath}`);
} catch (error) {
console.error(`Error processing file ${filePath}:`, error.message);
}
}
function processDirectory(dirPath) {
fs.readdirSync(dirPath).forEach(file => {
const filePath = path.join(dirPath, file);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
processDirectory(filePath);
} else if (path.extname(filePath) === '.js') {
if (filePath !== path.join(__dirname, 'build.js')) {
processFile(filePath);
}
}
});
}
console.log('Starting JavaScript build process...');
processDirectory(sourceDir);
console.log('Build complete. Files are in the /dist/js folder.');
+458
View File
@@ -0,0 +1,458 @@
@import "tailwindcss";
html {
scroll-behavior: smooth;
}
body {
font-family: 'DM Sans', sans-serif;
background-color: #111827;
color: #d1d5db;
}
.tool-card {
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
border: 1px solid #374151;
}
.tool-card:hover {
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);
border-color: #4f46e5;
}
.btn {
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
}
.btn:active {
transform: scale(0.98);
}
/* Custom file input */
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;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #1f2937;
}
::-webkit-scrollbar-thumb {
background: #4f46e5;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #4338ca;
/* indigo-700 */
}
/* Style for drag-and-drop placeholder */
.sortable-ghost {
opacity: 0.4;
border: 2px dashed #4f46e5;
}
#embed-pdf-container>div {
width: 100%;
height: 100%;
}
#tool-interface {
color: #39A0ED;
}
.page-thumbnail,
#file-list>li {
cursor: grab;
}
.sortable-chosen {
cursor: grabbing;
}
.compare-viewer-wrapper.overlay-mode {
position: relative;
width: 100%;
height: 75vh;
overflow: auto;
border: 2px solid #374151;
border-radius: 0.5rem;
background-color: #1f2937;
}
/* This rule now ONLY applies to canvases in overlay mode */
.compare-viewer-wrapper.overlay-mode canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: auto;
}
.compare-viewer-wrapper.side-by-side-mode {
display: flex;
gap: 1rem;
width: 100%;
}
.pdf-panel {
flex: 1;
min-width: 0;
overflow: auto;
height: 75vh;
border: 2px solid #374151;
border-radius: 0.5rem;
background-color: #1f2937;
}
/* This rule ensures canvases in side-by-side panels display at their natural rendered size. */
.pdf-panel canvas {
display: block;
margin: 0 auto;
}
footer a {
transition: color 0.2s ease-in-out;
}
.marker {
position: relative;
display: inline-block;
}
.marker::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 100%;
/* height: 30px; */
background-color: orange;
/* Yellow marker color */
z-index: -1;
transform: skew(-20deg);
}
.pill {
background-color: #374151;
/* bg-gray-700 */
color: #d1d5db;
/* text-gray-300 */
padding: 8px 16px;
border-radius: 9999px;
/* rounded-full */
font-size: 14px;
font-weight: 500;
}
.cta-button {
background-color: #4f46e5;
/* indigo-600 */
color: white;
font-weight: 600;
padding: 12px 24px;
border-radius: 8px;
/* rounded-lg */
transition: background-color 0.2s ease-in-out;
}
.cta-button:hover {
background-color: #4338ca;
/* indigo-700 */
}
.marker-text {
background-color: rgba(255, 255, 0, 0.5);
/* Yellow marker color */
padding: 0 5px;
}
.feature-card {
background-color: #1f2937;
/* bg-gray-800 */
padding: 24px;
border-radius: 8px;
/* rounded-lg */
text-align: center;
}
.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;
}
.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;
}
.marker-slanted {
position: relative;
display: inline-block;
}
.marker-slanted::before {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 6px;
background: linear-gradient(120deg, #6366f1, #8b5cf6);
z-index: -1;
}
.section-divider {
height: 1px;
background: linear-gradient(to right, transparent, #4f46e5, transparent);
/* Fades from transparent to indigo and back */
margin: 2rem auto;
/* my-16 */
max-width: 42rem;
/* max-w-xl */
opacity: 0.5;
}
.btn-gradient {
display: inline-block;
padding: 0.75rem 2rem;
/* py-3 px-8 */
border-radius: 0.5rem;
/* rounded-lg */
background-image: linear-gradient(to bottom, #6366f1, #4f46e5);
/* from-indigo-500 to-indigo-600 */
color: #ffffff;
/* text-white */
font-weight: 600;
/* font-semibold */
transition-property: all;
transition-duration: 200ms;
transform: translateY(0);
}
.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);
/* hover:shadow-xl hover:shadow-indigo-500/30 */
transform: translateY(-0.25rem);
/* hover:-translate-y-1 */
}
.btn-gradient:focus {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px #111827, 0 0 0 4px #818cf8;
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
}
.btn-gradient:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.solid-spinner {
width: 64px;
/* w-16 */
height: 64px;
/* h-16 */
border: 5px solid #374151;
/* border-gray-700 */
border-bottom-color: #4f46e5;
/* border-indigo-600 */
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#signature-ghost {
position: absolute;
z-index: 100;
pointer-events: none;
/* Allows clicks to pass through to the canvas */
opacity: 0.6;
background-size: contain;
background-repeat: no-repeat;
}
/* Highlight for selected signature in the saved list */
.saved-signature.selected {
border-color: #4f46e5;
/* indigo-600 */
box-shadow: 0 0 10px rgba(79, 70, 229, 0.5);
}
/* Cursor change when hovering over a placed signature */
#canvas-sign.movable {
cursor: move;
}
#canvas-sign.resize-ns {
cursor: ns-resize;
}
#canvas-sign.resize-ew {
cursor: ew-resize;
}
#canvas-sign.resize-nesw {
cursor: nesw-resize;
}
#canvas-sign.resize-nwse {
cursor: nwse-resize;
}
.faq-item.open .faq-question {
color: #818cf8;
/* indigo-400 */
}
.faq-item.open .faq-icon {
transform: rotate(180deg);
}
/* Testimonial Card Styles */
.testimonial-card {
background-color: #1f2937;
/* bg-gray-800 */
padding: 24px;
border-radius: 0.75rem;
/* rounded-xl */
border: 1px solid #374151;
/* border-gray-700 */
display: flex;
flex-direction: column;
}
.pill {
background-color: #374151;
/* bg-gray-700 */
color: #d1d5db;
/* text-gray-300 */
padding: 8px 16px;
border-radius: 9999px;
/* rounded-full */
font-size: 14px;
font-weight: 500;
}
.pill {
background-color: #374151;
/* bg-gray-700 */
color: #d1d5db;
/* text-gray-300 */
/* Smaller padding and font size by default for mobile */
padding: 6px 12px;
font-size: 12px;
border-radius: 9999px;
/* rounded-full */
font-weight: 500;
}
/* On small screens (640px) and up, revert to the larger size */
@media (min-width: 640px) {
.pill {
padding: 8px 16px;
font-size: 14px;
}
}
/* Ensure form-field-group contents don't overflow on small screens */
.form-field-group .capitalize {
word-break: break-all;
}
@media (max-width: 1023px) {
#form-fields-container {
overflow-x: auto;
white-space: nowrap;
}
.form-field-group {
display: inline-block;
width: 90%;
/* Adjust as needed */
margin-right: 1rem;
}
}
#page-merge-preview {
display: grid;
gap: 1rem;
padding: 1rem;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
.legal-content h2 {
font-size: 1.5rem;
line-height: 2rem;
font-weight: 700;
color: white;
margin-top: 2rem;
margin-bottom: 1rem;
}
.legal-content h3 {
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 600;
color: rgb(129 140 248);
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.legal-content p {
margin-bottom: 1rem;
line-height: 1.625;
color: rgb(156 163 175);
}
.legal-content ul {
list-style-type: disc;
list-style-position: inside;
margin-bottom: 1rem;
padding-left: 1rem;
color: rgb(156 163 175);
}
.legal-content a {
color: rgb(129 140 248);
text-decoration: underline;
}
.legal-content a:hover {
color: rgb(165 180 252);
}
details>summary {
list-style: none;
}
details>summary::-webkit-details-marker {
display: none;
}
details>summary .icon {
transition: transform 0.2s ease-in-out;
}
details[open]>summary .icon {
transform: rotate(45deg);
}
+273
View File
@@ -0,0 +1,273 @@
import { showLoader, hideLoader, showAlert } from './ui.js';
import { state } from './state.js';
import { toolLogic } from './logic/index.js';
import { icons, createIcons } from "lucide";
const editorState = {
pdf: null,
canvas: null,
context: null,
container: null,
currentPageNum: 1,
pageRendering: false,
pageNumPending: null,
scale: 1.0,
pageSnapshot: null,
isDrawing: false,
startX: 0,
startY: 0,
cropBoxes: {},
lastInteractionRect: null, // Used to store the rectangle from the last move event
};
/**
* Calculates the best scale to fit the page within the container.
* @param {PDFPageProxy} page - The PDF.js page object.
*/
function calculateFitScale(page: any) {
const containerWidth = editorState.container.clientWidth;
const viewport = page.getViewport({ scale: 1.0 });
return containerWidth / viewport.width;
}
/**
* Renders a specific page of the PDF onto the canvas.
* @param {number} num The page number to render.
*/
async function renderPage(num: any) {
editorState.pageRendering = true;
showLoader(`Loading page ${num}...`);
try {
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
if (editorState.scale === 'fit') {
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);
}
}
}
function queueRenderPage(num: any) {
if (editorState.pageRendering) {
editorState.pageNumPending = num;
} else {
editorState.currentPageNum = num;
renderPage(num);
}
}
function redrawShapes() {
if (editorState.pageSnapshot) {
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);
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() {
if (!editorState.isDrawing) return;
editorState.isDrawing = false;
const finalRect = editorState.lastInteractionRect;
if (!finalRect || finalRect.width < 5 || finalRect.height < 5) {
redrawShapes(); // Redraw to clear any invalid, tiny box
editorState.lastInteractionRect = null;
return;
}
editorState.cropBoxes[editorState.currentPageNum - 1] = {
...finalRect,
scale: editorState.scale
};
editorState.lastInteractionRect = null; // Reset for the next drawing action
redrawShapes();
}
export async function setupCanvasEditor(toolId: any) {
editorState.canvas = document.getElementById('canvas-editor');
if (!editorState.canvas) return;
editorState.container = document.getElementById('canvas-container');
editorState.context = editorState.canvas.getContext('2d');
const pageNav = document.getElementById('page-nav');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
editorState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
editorState.cropBoxes = {};
editorState.currentPageNum = 1;
// @ts-expect-error TS(2322) FIXME: Type 'string' is not assignable to type 'number'.
editorState.scale = 'fit';
pageNav.textContent = '';
const prevButton = document.createElement('button');
prevButton.id = 'prev-page';
prevButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
const pageInfo = document.createElement('span');
pageInfo.className = 'text-white font-medium';
const currentPageDisplay = document.createElement('span');
currentPageDisplay.id = 'current-page-display';
currentPageDisplay.textContent = '1';
pageInfo.append('Page ', currentPageDisplay, ` of ${editorState.pdf.numPages}`);
const nextButton = document.createElement('button');
nextButton.id = 'next-page';
nextButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
pageNav.append(prevButton, pageInfo, nextButton);
createIcons({icons});
document.getElementById('prev-page').addEventListener('click', () => {
if (editorState.currentPageNum > 1) 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
const newCanvas = editorState.canvas.cloneNode(true);
editorState.canvas.parentNode.replaceChild(newCanvas, editorState.canvas);
editorState.canvas = newCanvas;
editorState.context = newCanvas.getContext('2d');
// Mouse Events
editorState.canvas.addEventListener('mousedown', handleInteractionStart);
editorState.canvas.addEventListener('mousemove', handleInteractionMove);
editorState.canvas.addEventListener('mouseup', handleInteractionEnd);
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
// Touch Events
editorState.canvas.addEventListener('touchstart', handleInteractionStart, { passive: false });
editorState.canvas.addEventListener('touchmove', handleInteractionMove, { passive: false });
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
if (toolId === 'crop') {
document.getElementById('zoom-in-btn').onclick = () => {
editorState.scale += 0.25;
renderPage(editorState.currentPageNum);
};
document.getElementById('zoom-out-btn').onclick = () => {
if (editorState.scale > 0.25) {
editorState.scale -= 0.25;
renderPage(editorState.currentPageNum);
}
};
document.getElementById('fit-page-btn').onclick = async () => {
const page = await editorState.pdf.getPage(editorState.currentPageNum);
editorState.scale = calculateFitScale(page);
renderPage(editorState.currentPageNum);
};
document.getElementById('clear-crop-btn').onclick = () => {
delete editorState.cropBoxes[editorState.currentPageNum - 1];
redrawShapes();
};
document.getElementById('clear-all-crops-btn').onclick = () => {
editorState.cropBoxes = {};
redrawShapes();
};
document.getElementById('process-btn').onclick = async () => {
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.');
return;
}
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);
}
+17
View File
@@ -0,0 +1,17 @@
export const singlePdfLoadTools = [
'split', 'organize', 'rotate', 'add-page-numbers',
'pdf-to-jpg', 'pdf-to-png', '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',
'reverse-pages', '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'
];
export const simpleTools = [
'encrypt', 'decrypt', 'change-permissions', 'pdf-to-markdown', 'word-to-pdf',
];
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',
];
+22
View File
@@ -0,0 +1,22 @@
export const tesseractLanguages = {
"eng": "English", "afr": "Afrikaans", "amh": "Amharic", "ara": "Arabic", "asm": "Assamese", "aze": "Azerbaijani",
"aze_cyrl": "Azerbaijani - Cyrillic", "bel": "Belarusian", "ben": "Bengali", "bod": "Tibetan", "bos": "Bosnian",
"bul": "Bulgarian", "cat": "Catalan; Valencian", "ceb": "Cebuano", "ces": "Czech", "chi_sim": "Chinese - Simplified",
"chi_tra": "Chinese - Traditional", "chr": "Cherokee", "cym": "Welsh", "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"
};
+105
View File
@@ -0,0 +1,105 @@
// This file centralizes the definition of all available tools, organized by category.
export const categories = [
{
name: 'Popular 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: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs' },
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
]
},
{
name: 'Edit & Annotate',
tools: [
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.' },
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
{ id: '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: 'invert-colors', name: 'Invert Colors', icon: 'contrast', subtitle: 'Create a "dark mode" version of your PDF.' },
{ id: 'change-background-color', name: 'Background Color', icon: 'palette', subtitle: 'Change the background color of your PDF.' },
{ id: 'change-text-color', name: 'Change Text Color', icon: 'type', subtitle: 'Change the color of text in your PDF.' },
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
{ id: 'remove-annotations', name: 'Remove Annotations', icon: 'eraser', subtitle: 'Strip comments, highlights, and links.' },
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
{ id: 'form-filler', name: 'PDF Form Filler', icon: 'square-pen', subtitle: 'Fill in forms directly in the browser.' },
]
},
{
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: '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: '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: '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.' },
]
},
];
+413
View File
@@ -0,0 +1,413 @@
import { state } from '../state.js';
import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { setupCanvasEditor } from '../canvasEditor.js';
import { toolLogic } from '../logic/index.js';
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { icons, createIcons } from 'lucide';
import Sortable from 'sortablejs';
import { multiFileTools, simpleTools, singlePdfLoadTools } from '../config/pdf-tools.js';
import * as pdfjsLib from 'pdfjs-dist';
async function handleSinglePdfUpload(toolId, file) {
showLoader('Loading PDF...');
try {
const pdfBytes = await readFileAsArrayBuffer(file);
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
ignoreEncryption: true
});
hideLoader();
if (state.pdfDoc.isEncrypted && toolId !== 'decrypt' && toolId !== 'change-permissions') {
showAlert('Protected PDF', 'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.');
switchView('grid');
return;
}
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');
if (processBtn) {
(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 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 === '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) {
const fileInput = document.getElementById('file-input');
const isMultiFileTool = multiFileTools.includes(toolId);
let isFirstUpload = true;
const processFiles = async (newFiles) => {
if (newFiles.length === 0) return;
if (!isMultiFileTool || isFirstUpload) {
state.files = newFiles;
} else {
state.files = [...state.files, ...newFiles];
}
isFirstUpload = false;
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) {
renderFileDisplay(fileDisplayArea, state.files);
}
const fileControls = document.getElementById('file-controls');
if (fileControls) {
fileControls.classList.remove('hidden');
createIcons({ icons });
}
if (isMultiFileTool) {
handleMultiFileUpload(toolId);
} else if (singlePdfLoadTools.includes(toolId)) {
await handleSinglePdfUpload(toolId, state.files[0]);
} else if (simpleTools.includes(toolId)) {
const optionsDivId = toolId === 'change-permissions' ? 'permissions-options' : `${toolId}-options`;
const optionsDiv = document.getElementById(optionsDivId);
if (optionsDiv) optionsDiv.classList.remove('hidden');
const processBtn = document.getElementById('process-btn');
if (processBtn) {
(processBtn as HTMLButtonElement).disabled = false;
processBtn.onclick = () => {
const logic = toolLogic[toolId];
if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic;
func();
}
};
}
} else if (toolId === 'edit') {
const file = state.files[0];
if (!file) return;
const pdfWrapper = document.getElementById('embed-pdf-wrapper');
const pdfContainer = document.getElementById('embed-pdf-container');
pdfContainer.textContent = ''; // Clear safely
if (state.currentPdfUrl) {
URL.revokeObjectURL(state.currentPdfUrl);
}
pdfWrapper.classList.remove('hidden');
const fileURL = URL.createObjectURL(file);
state.currentPdfUrl = fileURL;
const script = document.createElement('script');
script.type = 'module';
script.textContent = `
import EmbedPDF from 'https://snippet.embedpdf.com/embedpdf.js';
EmbedPDF.init({
type: 'container',
target: document.getElementById('embed-pdf-container'),
src: '${fileURL}',
theme: 'dark',
});
`;
document.head.appendChild(script);
const backBtn = document.getElementById('back-to-grid');
const urlRevoker = () => {
URL.revokeObjectURL(fileURL);
state.currentPdfUrl = null;
backBtn.removeEventListener('click', urlRevoker);
};
backBtn.addEventListener('click', urlRevoker);
}
};
fileInput.addEventListener('change', (e) => processFiles(Array.from((e.target as HTMLInputElement).files || [])));
const setupAddMoreButton = () => {
const addMoreBtn = document.getElementById('add-more-btn');
if (addMoreBtn) {
addMoreBtn.addEventListener('click', () => fileInput.click());
}
};
const setupClearButton = () => {
const clearBtn = document.getElementById('clear-files-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
state.files = [];
isFirstUpload = true;
(fileInput as HTMLInputElement).value = '';
const fileDisplayArea = document.getElementById('file-display-area');
if (fileDisplayArea) fileDisplayArea.textContent = '';
const fileControls = document.getElementById('file-controls');
if (fileControls) fileControls.classList.add('hidden');
const toolSpecificUI = ['file-list', 'page-merge-preview', 'image-list'];
toolSpecificUI.forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = '';
});
const processBtn = document.getElementById('process-btn');
if (processBtn) (processBtn as HTMLButtonElement).disabled = true;
});
}
};
setTimeout(() => {
setupAddMoreButton();
setupClearButton();
}, 100);
}
+41
View File
@@ -0,0 +1,41 @@
import { state } from '../state.js';
import { dom, switchView, toolTemplates } from '../ui.js';
import { setupFileInputHandler } from './fileHandler.js';
import { toolLogic } from '../logic/index.js';
import { createIcons, icons } from 'lucide';
const SETUP_AFTER_UPLOAD = ['sign-pdf'];
export function setupToolInterface(toolId: any) {
window.scrollTo({
top: 0,
left: 0,
behavior: 'instant' as ScrollBehavior
});
state.activeTool = toolId;
dom.toolContent.innerHTML = toolTemplates[toolId]();
createIcons({icons});
switchView('tool');
const fileInput = document.getElementById('file-input');
const processBtn = document.getElementById('process-btn');
if (!fileInput && processBtn) {
const logic = toolLogic[toolId];
if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic;
processBtn.onclick = func;
}
}
if (toolLogic[toolId] && typeof toolLogic[toolId].setup === 'function') {
if (!SETUP_AFTER_UPLOAD.includes(toolId)) {
toolLogic[toolId].setup();
}
}
if (fileInput) {
setupFileInputHandler(toolId);
}
}
+51
View File
@@ -0,0 +1,51 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function addBlankPage() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageNumberInput = document.getElementById('page-number').value;
if (pageNumberInput.trim() === '') {
showAlert('Invalid Input', 'Please enter a page number.');
return;
}
const position = parseInt(pageNumberInput);
const totalPages = state.pdfDoc.getPageCount();
if (isNaN(position) || position < 0 || position > totalPages) {
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
return;
}
showLoader('Adding page...');
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));
}
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();
}
}
+101
View File
@@ -0,0 +1,101 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb, parsePageRanges } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
export function setupHeaderFooterUI() {
const totalPagesSpan = document.getElementById('total-pages');
if (totalPagesSpan && state.pdfDoc) {
totalPagesSpan.textContent = state.pdfDoc.getPageCount();
}
}
export async function addHeaderFooter() {
showLoader('Adding header & footer...');
try {
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
const allPages = state.pdfDoc.getPages();
const totalPages = allPages.length;
const margin = 40;
// --- 1. Get new formatting options from the UI ---
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 10;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('font-color').value;
const fontColor = hexToRgb(colorHex);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageRangeInput = document.getElementById('page-range').value;
// --- 2. Get text values ---
const texts = {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerLeft: document.getElementById('header-left').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerCenter: document.getElementById('header-center').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
headerRight: document.getElementById('header-right').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerLeft: document.getElementById('footer-left').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerCenter: document.getElementById('footer-center').value,
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
footerRight: document.getElementById('footer-right').value,
};
// --- 3. Parse page range to determine which pages to modify ---
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (indicesToProcess.length === 0) {
throw new Error("Invalid page range specified. Please check your input (e.g., '1-3, 5').");
}
// --- 4. Define drawing options with new values ---
const drawOptions = {
font: helveticaFont,
size: fontSize,
color: rgb(fontColor.r, fontColor.g, fontColor.b)
};
// --- 5. Loop over only the selected pages ---
for (const pageIndex of indicesToProcess) {
// @ts-expect-error TS(2538) FIXME: Type 'unknown' cannot be used as an index type.
const page = allPages[pageIndex];
const { width, height } = page.getSize();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
const pageNumber = pageIndex + 1; // For dynamic text
// Helper to replace placeholders like {page} and {total}
const processText = (text: any) => text
.replace(/{page}/g, pageNumber)
.replace(/{total}/g, totalPages);
// Get processed text for the current page
const processedTexts = {
headerLeft: processText(texts.headerLeft),
headerCenter: processText(texts.headerCenter),
headerRight: processText(texts.headerRight),
footerLeft: processText(texts.footerLeft),
footerCenter: processText(texts.footerCenter),
footerRight: processText(texts.footerRight),
};
if (processedTexts.headerLeft) page.drawText(processedTexts.headerLeft, { ...drawOptions, x: margin, y: height - margin });
if (processedTexts.headerCenter) page.drawText(processedTexts.headerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.headerCenter, fontSize) / 2, y: height - margin });
if (processedTexts.headerRight) page.drawText(processedTexts.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.headerRight, fontSize), y: height - margin });
if (processedTexts.footerLeft) page.drawText(processedTexts.footerLeft, { ...drawOptions, x: margin, y: margin });
if (processedTexts.footerCenter) page.drawText(processedTexts.footerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.footerCenter, fontSize) / 2, y: margin });
if (processedTexts.footerRight) page.drawText(processedTexts.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.footerRight, fontSize), y: margin });
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'header-footer-added.pdf');
} catch (e) {
console.error(e);
showAlert('Error', e.message || 'Could not add header or footer.');
} finally {
hideLoader();
}
}
+101
View File
@@ -0,0 +1,101 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { rgb, StandardFonts } from 'pdf-lib';
export async function addPageNumbers() {
showLoader('Adding page numbers...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const position = document.getElementById('position').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const fontSize = parseInt(document.getElementById('font-size').value) || 12;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const format = document.getElementById('number-format').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('text-color').value;
const textColor = hexToRgb(colorHex);
const pages = state.pdfDoc.getPages();
const totalPages = pages.length;
const helveticaFont = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
for (let i = 0; i < totalPages; i++) {
const page = pages[i];
const mediaBox = page.getMediaBox();
const cropBox = page.getCropBox();
const bounds = cropBox || mediaBox;
const width = bounds.width;
const height = bounds.height;
const xOffset = bounds.x || 0;
const yOffset = bounds.y || 0;
let pageNumText = (format === 'page_x_of_y') ? `${i + 1} / ${totalPages}` : `${i + 1}`;
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
const textHeight = fontSize;
const minMargin = 8;
const maxMargin = 40;
const marginPercentage = 0.04;
const horizontalMargin = Math.max(minMargin, Math.min(maxMargin, width * marginPercentage));
const verticalMargin = Math.max(minMargin, Math.min(maxMargin, height * marginPercentage));
// Ensure text doesn't go outside visible page boundaries
const safeHorizontalMargin = Math.max(horizontalMargin, textWidth / 2 + 3);
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
let x, y;
switch (position) {
case 'bottom-center':
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-left':
x = safeHorizontalMargin + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'bottom-right':
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
y = safeVerticalMargin + yOffset;
break;
case 'top-center':
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-left':
x = safeHorizontalMargin + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
case 'top-right':
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
y = height - safeVerticalMargin - textHeight + yOffset;
break;
}
// Final safety check to ensure coordinates are within visible page bounds
x = Math.max(xOffset + 3, Math.min(xOffset + width - textWidth - 3, x));
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
page.drawText(pageNumText, {
x, y,
font: helveticaFont,
size: fontSize,
color: rgb(textColor.r, textColor.g, textColor.b)
});
}
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'paginated.pdf');
showAlert('Success', 'Page numbers added successfully!');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not add page numbers.');
} finally {
hideLoader();
}
}
+132
View File
@@ -0,0 +1,132 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, degrees, StandardFonts } from 'pdf-lib';
export function setupWatermarkUI() {
const watermarkTypeRadios = document.querySelectorAll('input[name="watermark-type"]');
const textOptions = document.getElementById('text-watermark-options');
const imageOptions = document.getElementById('image-watermark-options');
watermarkTypeRadios.forEach(radio => {
radio.addEventListener('change', (e) => {
// @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') {
textOptions.classList.remove('hidden');
imageOptions.classList.add('hidden');
} else {
textOptions.classList.add('hidden');
imageOptions.classList.remove('hidden');
}
});
});
const opacitySliderText = document.getElementById('opacity-text');
const opacityValueText = document.getElementById('opacity-value-text');
const angleSliderText = document.getElementById('angle-text');
const angleValueText = document.getElementById('angle-value-text');
// @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('input', () => opacityValueText.textContent = opacitySliderText.value);
// @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);
const opacitySliderImage = document.getElementById('opacity-image');
const opacityValueImage = document.getElementById('opacity-value-image');
const angleSliderImage = document.getElementById('angle-image');
const angleValueImage = document.getElementById('angle-value-image');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
opacitySliderImage.addEventListener('input', () => opacityValueImage.textContent = opacitySliderImage.value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
angleSliderImage.addEventListener('input', () => angleValueImage.textContent = angleSliderImage.value);
}
export async function addWatermark() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const watermarkType = document.querySelector('input[name="watermark-type"]:checked').value;
showLoader('Adding watermark...');
try {
const pages = state.pdfDoc.getPages();
let watermarkAsset = null;
if (watermarkType === 'text') {
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
} else { // 'image'
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const imageFile = document.getElementById('image-watermark-input').files[0];
if (!imageFile) throw new Error('Please select an image file for the watermark.');
const imageBytes = await readFileAsArrayBuffer(imageFile);
if (imageFile.type === 'image/png') {
watermarkAsset = await state.pdfDoc.embedPng(imageBytes);
} else if (imageFile.type === 'image/jpeg') {
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
} else {
throw new Error('Unsupported Image. Please use a PNG or JPG for the watermark.');
}
}
for (const page of pages) {
const { width, height } = page.getSize();
if (watermarkType === 'text') {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const text = document.getElementById('watermark-text').value;
if (!text.trim()) throw new Error('Please enter text for the watermark.');
// @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();
}
}
+55
View File
@@ -0,0 +1,55 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
async function convertImageToPngBytes(file: any) {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
resolve(pngBytes);
};
img.onerror = () => reject(new Error('Failed to load image.'));
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsDataURL(file);
});
}
export async function bmpToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one BMP file.');
return;
}
showLoader('Converting BMP to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await convertImageToPngBytes(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_bmps.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert BMP to PDF. One of the files may be invalid.');
} finally {
hideLoader();
}
}
+53
View File
@@ -0,0 +1,53 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
export async function changeBackgroundColor() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const colorHex = document.getElementById('background-color').value;
const color = hexToRgb(colorHex);
showLoader('Changing background color...');
try {
const newPdfDoc = await PDFLibDocument.create();
for (let i = 0; i < state.pdfDoc.getPageCount(); i++) {
const [originalPage] = await newPdfDoc.copyPages(state.pdfDoc, [i]);
const { width, height } = originalPage.getSize();
const newPage = newPdfDoc.addPage([width, height]);
newPage.drawRectangle({
x: 0,
y: 0,
width,
height,
color: rgb(color.r, color.g, color.b),
});
const embeddedPage = await newPdfDoc.embedPage(originalPage);
newPage.drawPage(embeddedPage, {
x: 0,
y: 0,
width,
height,
});
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not change the background color.');
} finally {
hideLoader();
}
}
+115
View File
@@ -0,0 +1,115 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
import blobStream from 'blob-stream';
import * as pdfjsLib from "pdfjs-dist";
export async function changePermissions() {
const currentPassword = (document.getElementById('current-password') as HTMLInputElement).value;
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement).value;
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement).value;
// An owner password is required to enforce any permissions.
if (!newOwnerPassword && (newUserPassword || document.querySelectorAll('input[type="checkbox"]:not(:checked)').length > 0)) {
showAlert('Input Required', 'You must set a "New Owner Password" to enforce specific permissions or to set a user password.');
return;
}
showLoader('Preparing to process...');
try {
const file = state.files[0];
const pdfData = await readFileAsArrayBuffer(file);
let pdf;
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) {
console.error(e);
hideLoader();
showAlert('Error', `An unexpected error occurred: ${e.message}`);
}
}
+144
View File
@@ -0,0 +1,144 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
let isRenderingPreview = false;
let renderTimeout: any;
async function updateTextColorPreview() {
if (isRenderingPreview) return;
isRenderingPreview = true;
try {
const textColorCanvas = document.getElementById('text-color-canvas');
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'.
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;
// @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: originalCanvas.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();
}
}
+77
View File
@@ -0,0 +1,77 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
export async function combineToSinglePage() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const spacing = parseInt(document.getElementById('page-spacing').value) || 0;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const backgroundColorHex = document.getElementById('background-color').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addSeparator = document.getElementById('add-separator').checked;
const backgroundColor = hexToRgb(backgroundColorHex);
showLoader('Combining pages...');
try {
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
let maxWidth = 0;
let totalHeight = 0;
sourcePages.forEach((page: any) => {
const { width, height } = page.getSize();
if (width > maxWidth) maxWidth = width;
totalHeight += height;
});
totalHeight += Math.max(0, sourcePages.length - 1) * spacing;
const newPage = newDoc.addPage([maxWidth, totalHeight]);
if (backgroundColorHex.toUpperCase() !== '#FFFFFF') {
newPage.drawRectangle({
x: 0,
y: 0,
width: maxWidth,
height: totalHeight,
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();
}
}
+208
View File
@@ -0,0 +1,208 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { icons, createIcons } from "lucide";
const state = {
pdfDoc1: null,
pdfDoc2: null,
currentPage: 1,
viewMode: 'overlay',
isSyncScroll: true,
};
/**
* Renders a PDF page to fit the width of its container.
* @param {PDFDocumentProxy} pdfDoc - The loaded PDF document from pdf.js.
* @param {number} pageNum - The page number to render.
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
* @param {HTMLElement} container - The container to fit the canvas into.
*/
async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any) {
const page = await pdfDoc.getPage(pageNum);
// Calculate scale to fit the container width.
const containerWidth = container.clientWidth - 2; // Subtract border width
const viewport = page.getViewport({ scale: 1.0 });
const scale = containerWidth / viewport.width;
const scaledViewport = page.getViewport({ scale: scale });
canvas.width = scaledViewport.width;
canvas.height = scaledViewport.height;
await page.render({
canvasContext: canvas.getContext('2d'),
viewport: scaledViewport,
}).promise;
}
async function renderBothPages() {
if (!state.pdfDoc1 || !state.pdfDoc2) return;
showLoader(`Loading page ${state.currentPage}...`);
const canvas1 = document.getElementById('canvas-compare-1');
const canvas2 = document.getElementById('canvas-compare-2');
const panel1 = document.getElementById('panel-1');
const panel2 = document.getElementById('panel-2');
const wrapper = document.getElementById('compare-viewer-wrapper');
// Determine the correct container based on the view mode
const container1 = state.viewMode === 'overlay' ? wrapper : panel1;
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
await Promise.all([
renderPage(state.pdfDoc1, Math.min(state.currentPage, state.pdfDoc1.numPages), canvas1, container1),
renderPage(state.pdfDoc2, Math.min(state.currentPage, state.pdfDoc2.numPages), canvas2, container2)
]);
updateNavControls();
hideLoader();
}
function updateNavControls() {
const maxPages = Math.max(state.pdfDoc1?.numPages || 0, state.pdfDoc2?.numPages || 0);
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
document.getElementById('current-page-display-compare').textContent = state.currentPage;
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
document.getElementById('total-pages-display-compare').textContent = maxPages;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('prev-page-compare').disabled = state.currentPage <= 1;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
document.getElementById('next-page-compare').disabled = state.currentPage >= maxPages;
}
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
const fileInput = document.getElementById(inputId);
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
const handleFile = async (file: any) => {
if (!file || file.type !== 'application/pdf') return showAlert('Invalid File', 'Please select a valid PDF file.');
const displayDiv = document.getElementById(displayId);
displayDiv.textContent = '';
// 2. Create the icon element
const icon = document.createElement('i');
icon.setAttribute('data-lucide', 'check-circle');
icon.className = 'w-10 h-10 mb-3 text-green-500';
// 3. Create the paragraph element for the file name
const p = document.createElement('p');
p.className = 'text-sm text-gray-300 truncate';
// 4. Set the file name safely using textContent
p.textContent = file.name;
// 5. Append the safe elements to the container
displayDiv.append(icon, p);
createIcons({ icons });
try {
showLoader(`Loading ${file.name}...`);
const pdfBytes = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
state[docKey] = await pdfjsLib.getDocument(pdfBytes).promise;
if (state.pdfDoc1 && state.pdfDoc2) {
document.getElementById('compare-viewer').classList.remove('hidden');
state.currentPage = 1;
await renderBothPages();
}
} catch (e) {
showAlert('Error', 'Could not load PDF. It may be corrupt or password-protected.');
console.error(e);
} finally {
hideLoader();
}
};
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
dropZone.addEventListener('dragover', (e) => e.preventDefault());
dropZone.addEventListener('drop', (e) => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); });
}
/**
* Toggles the UI between Overlay and Side-by-Side views.
* @param {'overlay' | 'side-by-side'} mode
*/
function setViewMode(mode: any) {
state.viewMode = mode;
const wrapper = document.getElementById('compare-viewer-wrapper');
const overlayControls = document.getElementById('overlay-controls');
const sideControls = document.getElementById('side-by-side-controls');
const btnOverlay = document.getElementById('view-mode-overlay');
const btnSide = document.getElementById('view-mode-side');
const canvas2 = document.getElementById('canvas-compare-2');
const opacitySlider = document.getElementById('opacity-slider');
if (mode === 'overlay') {
wrapper.className = 'compare-viewer-wrapper overlay-mode';
overlayControls.classList.remove('hidden');
sideControls.classList.add('hidden');
btnOverlay.classList.add('bg-indigo-600');
btnSide.classList.remove('bg-indigo-600');
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
canvas2.style.opacity = opacitySlider.value;
} else {
wrapper.className = 'compare-viewer-wrapper side-by-side-mode';
overlayControls.classList.add('hidden');
sideControls.classList.remove('hidden');
btnOverlay.classList.remove('bg-indigo-600');
btnSide.classList.add('bg-indigo-600');
// CHANGE: When switching to side-by-side, reset the canvas opacity to 1.
canvas2.style.opacity = '1';
}
renderBothPages();
}
export function setupCompareTool() {
setupFileInput('file-input-1', 'pdfDoc1', 'file-display-1');
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
document.getElementById('prev-page-compare').addEventListener('click', () => {
if (state.currentPage > 1) { state.currentPage--; renderBothPages(); }
});
document.getElementById('next-page-compare').addEventListener('click', () => {
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
if (state.currentPage < maxPages) { state.currentPage++; renderBothPages(); }
});
document.getElementById('view-mode-overlay').addEventListener('click', () => setViewMode('overlay'));
document.getElementById('view-mode-side').addEventListener('click', () => setViewMode('side-by-side'));
const canvas2 = document.getElementById('canvas-compare-2');
document.getElementById('flicker-btn').addEventListener('click', () => {
canvas2.style.transition = 'opacity 150ms ease-in-out';
canvas2.style.opacity = (canvas2.style.opacity === '0') ? '1' : '0';
});
document.getElementById('opacity-slider').addEventListener('input', (e) => {
canvas2.style.transition = '';
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
canvas2.style.opacity = e.target.value;
});
const panel1 = document.getElementById('panel-1');
const panel2 = document.getElementById('panel-2');
const syncToggle = document.getElementById('sync-scroll-toggle');
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
syncToggle.addEventListener('change', () => { state.isSyncScroll = syncToggle.checked; });
let scrollingPanel: any = null;
panel1.addEventListener('scroll', () => {
if (state.isSyncScroll && scrollingPanel !== panel2) {
scrollingPanel = panel1;
panel2.scrollTop = panel1.scrollTop;
setTimeout(() => scrollingPanel = null, 100);
}
});
panel2.addEventListener('scroll', () => {
if (state.isSyncScroll && scrollingPanel !== panel1) {
scrollingPanel = panel2;
panel1.scrollTop = panel2.scrollTop;
setTimeout(() => scrollingPanel = null, 100);
}
});
}
+279
View File
@@ -0,0 +1,279 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
import { state } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
function dataUrlToBytes(dataUrl: any) {
const base64 = dataUrl.split(',')[1];
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
async function performSmartCompression(arrayBuffer: any, settings: any) {
const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
const pages = pdfDoc.getPages();
if (settings.removeMetadata) {
try {
pdfDoc.setTitle('');
pdfDoc.setAuthor('');
pdfDoc.setSubject('');
pdfDoc.setKeywords([]);
pdfDoc.setCreator('');
pdfDoc.setProducer('');
} catch (e) {
console.warn('Could not remove metadata:', e);
}
}
for (let i = 0; i < pages.length; i++) {
const page = pages[i];
const resources = page.node.Resources();
if (!resources) continue;
const xobjects = resources.lookup(PDFName.of('XObject'));
if (!(xobjects instanceof PDFDict)) continue;
for (const [key, value] of xobjects.entries()) {
const stream = pdfDoc.context.lookup(value);
if (!(stream instanceof PDFStream) || stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')) continue;
try {
const imageBytes = stream.getContents();
if (imageBytes.length < settings.skipSize) continue;
const width = stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
: 0;
const height = stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
: 0;
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) {
let newWidth = width;
let newHeight = height;
const scaleFactor = settings.scaleFactor || 1.0;
newWidth = Math.floor(width * scaleFactor);
newHeight = Math.floor(height * scaleFactor);
if (newWidth > settings.maxWidth || newHeight > settings.maxHeight) {
const aspectRatio = newWidth / newHeight;
if (newWidth > newHeight) {
newWidth = Math.min(newWidth, settings.maxWidth);
newHeight = newWidth / aspectRatio;
} else {
newHeight = Math.min(newHeight, settings.maxHeight);
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 saveOptions = {
useObjectStreams: settings.useObjectStreams !== false,
addDefaultPage: false,
objectsPerTick: settings.objectsPerTick || 50
};
return await pdfDoc.save(saveOptions);
}
async function performLegacyCompression(arrayBuffer: any, settings: any) {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const pdfJsDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const newPdfDoc = await PDFDocument.create();
for (let i = 1; i <= pdfJsDoc.numPages; i++) {
const page = await pdfJsDoc.getPage(i);
const viewport = page.getViewport({ scale: settings.scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport, canvas: canvas }).promise;
const jpegBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', settings.quality));
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const jpegBytes = await jpegBlob.arrayBuffer();
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
newPage.drawImage(jpegImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
}
return await newPdfDoc.save();
}
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
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
const algorithm = document.getElementById('compression-algorithm').value;
const settings = {
'balanced': {
smart: { quality: 0.5, threshold: 0.95, maxWidth: 1800, maxHeight: 1800, skipSize: 3000 },
legacy: { scale: 1.5, quality: 0.6 }
},
'high-quality': {
smart: { quality: 0.70, threshold: 0.98, 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 legacySettings = settings[level].legacy;
try {
const originalFile = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
let resultBytes;
let usedMethod;
if (algorithm === 'vector') {
showLoader('Running Vector (Smart) compression...');
resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
usedMethod = 'Vector';
} else if (algorithm === 'photon') {
showLoader('Running Photon (Rasterize) compression...');
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
usedMethod = 'Photon';
} else {
showLoader('Running Automatic (Vector first)...');
const vectorResultBytes = await performSmartCompression(arrayBuffer, smartSettings);
if (vectorResultBytes.length < originalFile.size) {
resultBytes = vectorResultBytes;
usedMethod = 'Vector (Automatic)';
} else {
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
showAlert('Vector failed to reduce size. Trying Photon...', 'info', 3000);
showLoader('Running Automatic (Photon fallback)...');
resultBytes = await performLegacyCompression(arrayBuffer, 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();
}
}
+309
View File
@@ -0,0 +1,309 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import Cropper from "cropperjs";
import * as pdfjsLib from 'pdfjs-dist';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
// --- Global State for the Cropper Tool ---
const cropperState = {
pdfDoc: null,
currentPageNum: 1,
cropper: null,
originalPdfBytes: null,
cropperImageElement: null,
pageCrops: {},
};
/**
* Saves the current crop data to the state object.
*/
function saveCurrentCrop() {
if (cropperState.cropper) {
const currentCrop = cropperState.cropper.getData(true);
const imageData = cropperState.cropper.getImageData();
const cropPercentages = {
x: currentCrop.x / imageData.naturalWidth,
y: currentCrop.y / imageData.naturalHeight,
width: currentCrop.width / imageData.naturalWidth,
height: currentCrop.height / imageData.naturalHeight,
};
cropperState.pageCrops[cropperState.currentPageNum] = cropPercentages;
}
}
/**
* Renders a PDF page to the Cropper UI as an image.
* @param {number} num The page number to render.
*/
async function displayPageAsImage(num: any) {
showLoader(`Rendering Page ${num}...`);
try {
const page = await cropperState.pdfDoc.getPage(num);
const viewport = page.getViewport({ scale: 2.5 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
if (cropperState.cropper) {
cropperState.cropper.destroy();
}
const 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();
}
}
/**
* Handles page navigation.
* @param {number} offset -1 for previous, 1 for next.
*/
async function changePage(offset: any) {
// Save the current page's crop before changing
saveCurrentCrop();
const newPageNum = cropperState.currentPageNum + offset;
if (newPageNum > 0 && newPageNum <= cropperState.pdfDoc.numPages) {
cropperState.currentPageNum = newPageNum;
await displayPageAsImage(cropperState.currentPageNum);
}
}
function updatePageInfo() {
document.getElementById('page-info').textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
}
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
document.getElementById('prev-page').disabled = cropperState.currentPageNum <= 1;
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
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
document.getElementById('crop-button').disabled = false;
}
/**
* Performs a non-destructive crop by updating the page's crop box.
*/
async function performMetadataCrop(pdfToModify: any, cropData: any) {
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
const page = pdfToModify.getPages()[pageNum - 1];
const { width: pageWidth, height: pageHeight } = page.getSize();
const rotation = page.getRotation().angle;
const crop = cropData[pageNum];
const visualPdfWidth = pageWidth * crop.width;
const visualPdfHeight = pageHeight * crop.height;
const visualPdfX = pageWidth * crop.x;
const visualPdfY = pageHeight * crop.y;
let finalX, finalY, finalWidth, finalHeight;
switch (rotation) {
case 90:
finalX = visualPdfY;
finalY = pageWidth - visualPdfX - visualPdfWidth;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
case 180:
finalX = pageWidth - visualPdfX - visualPdfWidth;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
case 270:
finalX = pageHeight - visualPdfY - visualPdfHeight;
finalY = visualPdfX;
finalWidth = visualPdfHeight;
finalHeight = visualPdfWidth;
break;
default:
finalX = visualPdfX;
finalY = pageHeight - visualPdfY - visualPdfHeight;
finalWidth = visualPdfWidth;
finalHeight = visualPdfHeight;
break;
}
page.setCropBox(finalX, finalY, finalWidth, finalHeight);
}
}
/**
* Performs a destructive crop by flattening the selected area to an image.
*/
async function performFlatteningCrop(cropData: any) {
const newPdfDoc = await PDFLibDocument.create();
// Load the original PDF with pdf-lib to copy un-cropped pages from
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes);
const totalPages = cropperState.pdfDoc.numPages;
for (let i = 0; i < totalPages; i++) {
const pageNum = i + 1;
showLoader(`Processing page ${pageNum} of ${totalPages}...`);
if (cropData[pageNum]) {
const page = await cropperState.pdfDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 2.5 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
const finalCanvas = document.createElement('canvas');
const finalCtx = finalCanvas.getContext('2d');
const crop = cropData[pageNum];
const finalWidth = tempCanvas.width * crop.width;
const finalHeight = tempCanvas.height * crop.height;
finalCanvas.width = finalWidth;
finalCanvas.height = finalHeight;
finalCtx.drawImage(
tempCanvas,
tempCanvas.width * crop.x, tempCanvas.height * crop.y,
finalWidth, finalHeight,
0, 0, finalWidth, finalHeight
);
const pngBytes = await new Promise(res => finalCanvas.toBlob(blob => blob.arrayBuffer().then(res), 'image/png'));
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight });
} else {
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
newPdfDoc.addPage(copiedPage);
}
}
return newPdfDoc;
}
export async function setupCropperTool() {
if (state.files.length === 0) return;
// Clear pageCrops on new file upload
cropperState.pageCrops = {};
const arrayBuffer = await readFileAsArrayBuffer(state.files[0]);
cropperState.originalPdfBytes = arrayBuffer;
const arrayBufferForPdfJs = (arrayBuffer as ArrayBuffer).slice(0);
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
try {
const loadingTask = pdfjsLib.getDocument({ data: arrayBufferForPdfJs });
cropperState.pdfDoc = await loadingTask.promise;
cropperState.currentPageNum = 1;
await displayPageAsImage(cropperState.currentPageNum);
document.getElementById('prev-page').addEventListener('click', () => changePage(-1));
document.getElementById('next-page').addEventListener('click', () => changePage(1));
document.getElementById('crop-button').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 = document.getElementById('destructive-crop-toggle').checked;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const isApplyToAll = document.getElementById('apply-to-all-toggle').checked;
let finalCropData = {};
if (isApplyToAll) {
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
if (!currentCrop) {
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++) {
finalCropData[i] = currentCrop;
}
} else {
// If not applying to all, only process pages with saved crops
finalCropData = Object.keys(cropperState.pageCrops).reduce((obj, key) => {
obj[key] = cropperState.pageCrops[key];
return obj;
}, {});
}
if (Object.keys(finalCropData).length === 0) {
showAlert('No Crop Area', 'Please select an area on at least one page to crop.');
return;
}
showLoader('Applying crop...');
try {
let finalPdfBytes;
if (isDestructive) {
const newPdfDoc = await performFlatteningCrop(finalCropData);
finalPdfBytes = await newPdfDoc.save();
} else {
const pdfToModify = await PDFLibDocument.load(cropperState.originalPdfBytes);
await performMetadataCrop(pdfToModify, finalCropData);
finalPdfBytes = await pdfToModify.save();
}
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
downloadFile(new Blob([finalPdfBytes], { type: 'application/pdf' }), fileName);
showAlert('Success', 'Crop complete! Your download has started.');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred during cropping.');
} finally {
hideLoader();
}
});
} catch (error) {
console.error("Error setting up cropper tool:", error);
showAlert('Error', 'Failed to load PDF for cropping.');
}
}
+65
View File
@@ -0,0 +1,65 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
import blobStream from 'blob-stream';
import * as pdfjsLib from "pdfjs-dist";
export async function decrypt() {
const file = state.files[0];
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const password = document.getElementById('password-input').value;
if (!password.trim()) {
showAlert('Input Required', 'Please enter the PDF password.');
return;
}
try {
showLoader('Preparing to process...');
const pdfData = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData, password: password }).promise;
const numPages = pdf.numPages;
const pageImages = [];
for (let i = 1; i <= numPages; i++) {
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
});
}
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.');
}
}
}
+57
View File
@@ -0,0 +1,57 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function deletePages() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageInput = document.getElementById('pages-to-delete').value;
if (!pageInput) {
showAlert('Invalid Input', 'Please enter page numbers to delete.');
return;
}
showLoader('Deleting pages...');
try {
const totalPages = state.pdfDoc.getPageCount();
const indicesToDelete = new Set();
const ranges = pageInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToDelete.add(pageNum - 1);
}
}
if (indicesToDelete.size === 0) {
showAlert('Invalid Input', 'No valid pages selected for deletion.');
hideLoader();
return;
}
if (indicesToDelete.size >= totalPages) {
showAlert('Invalid Input', 'You cannot delete all pages.');
hideLoader();
return;
}
const indicesToKeep = Array.from({ length: totalPages }, (_, i) => i).filter(index => !indicesToDelete.has(index));
const newPdf = await PDFLibDocument.create();
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'deleted-pages.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not delete pages.');
} finally {
hideLoader();
}
}
+164
View File
@@ -0,0 +1,164 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import Sortable from 'sortablejs'
import {icons, createIcons} from "lucide";
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
const duplicateOrganizeState = {
sortableInstances: {}
};
function initializePageGridSortable() {
const grid = document.getElementById('page-grid');
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 '{}'.
duplicateOrganizeState.sortableInstances.pageGrid.destroy();
}
// @ts-expect-error TS(2339) FIXME: Property 'pageGrid' does not exist on type '{}'.
duplicateOrganizeState.sortableInstances.pageGrid = Sortable.create(grid, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
filter: '.duplicate-btn, .delete-btn',
preventOnFilter: true,
onStart: function(evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function(evt: any) {
evt.item.style.opacity = '1';
}
});
}
/**
* Attaches event listeners for duplicate and delete to a page thumbnail element.
* @param {HTMLElement} element The thumbnail element to attach listeners to.
*/
function attachEventListeners(element: any) {
// Re-number all visible page labels
const renumberPages = () => {
const grid = document.getElementById('page-grid');
const pages = grid.querySelectorAll('.page-number');
pages.forEach((label, index) => {
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
label.textContent = index + 1;
});
};
// Duplicate button listener
element.querySelector('.duplicate-btn').addEventListener('click', (e: any) => {
e.stopPropagation();
const clone = element.cloneNode(true);
element.after(clone);
attachEventListeners(clone);
renumberPages();
initializePageGridSortable();
});
element.querySelector('.delete-btn').addEventListener('click', (e: any) => {
e.stopPropagation();
if (document.getElementById('page-grid').children.length > 1) {
element.remove();
renumberPages();
initializePageGridSortable();
} else {
showAlert('Cannot Delete', 'You cannot delete the last page of the document.');
}
});
}
export async function renderDuplicateOrganizeThumbnails() {
const grid = document.getElementById('page-grid');
if (!grid) return;
showLoader('Rendering page previews...');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
grid.textContent = '';
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 0.5 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
const wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.originalPageIndex = i - 1;
const imgContainer = document.createElement('div');
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'max-w-full max-h-full object-contain';
imgContainer.appendChild(img);
const 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.textContent = i.toString();
const controlsDiv = document.createElement('div');
controlsDiv.className = 'flex items-center justify-center gap-4';
const duplicateBtn = document.createElement('button');
duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
duplicateBtn.title = 'Duplicate Page';
const duplicateIcon = document.createElement('i');
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
duplicateIcon.className = 'w-5 h-5';
duplicateBtn.appendChild(duplicateIcon);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
deleteBtn.title = 'Delete Page';
const deleteIcon = document.createElement('i');
deleteIcon.setAttribute('data-lucide', 'x-circle');
deleteIcon.className = 'w-5 h-5';
deleteBtn.appendChild(deleteIcon);
controlsDiv.append(duplicateBtn, deleteBtn);
wrapper.append(imgContainer, pageNumberSpan, controlsDiv);
grid.appendChild(wrapper);
attachEventListeners(wrapper);
}
initializePageGridSortable();
createIcons({icons});
hideLoader();
}
export async function processAndSave() {
showLoader('Building new PDF...');
try {
const grid = document.getElementById('page-grid');
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 => parseInt(el.dataset.originalPageIndex));
const newPdfDoc = await PDFLibDocument.create();
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to save the new PDF.');
} finally {
hideLoader();
}
}
+73
View File
@@ -0,0 +1,73 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFName, PDFString } from "pdf-lib"
export async function editMetadata() {
showLoader('Updating metadata...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setTitle(document.getElementById('meta-title').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setAuthor(document.getElementById('meta-author').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setSubject(document.getElementById('meta-subject').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setCreator(document.getElementById('meta-creator').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
state.pdfDoc.setProducer(document.getElementById('meta-producer').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const keywords = document.getElementById('meta-keywords').value;
state.pdfDoc.setKeywords(keywords.split(',').map((k: any) => k.trim()).filter(Boolean));
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const creationDate = document.getElementById('meta-creation-date').value;
if (creationDate) {
state.pdfDoc.setCreationDate(new Date(creationDate));
}
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const modDate = document.getElementById('meta-mod-date').value;
if (modDate) {
state.pdfDoc.setModificationDate(new Date(modDate));
} else {
state.pdfDoc.setModificationDate(new Date());
}
const infoDict = state.pdfDoc.getInfoDict();
const standardKeys = new Set(['Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', 'CreationDate', 'ModDate']);
const allKeys = infoDict.keys().map((key: any) => key.asString().substring(1)); // Clean keys
allKeys.forEach((key: any) => {
if (!standardKeys.has(key)) {
infoDict.delete(PDFName.of(key));
}
});
const customKeys = document.querySelectorAll('.custom-meta-key');
const customValues = document.querySelectorAll('.custom-meta-value');
customKeys.forEach((keyInput, index) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const key = keyInput.value.trim();
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const value = customValues[index].value.trim();
if (key && value) {
// Now we add the fields to a clean slate
infoDict.set(PDFName.of(key), PDFString.of(value));
}
});
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-edited.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
} finally {
hideLoader();
}
}
+73
View File
@@ -0,0 +1,73 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
import blobStream from 'blob-stream';
import * as pdfjsLib from "pdfjs-dist";
export async function encrypt() {
const file = state.files[0];
const password = (document.getElementById('password-input') as HTMLInputElement).value;
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
});
}
document.getElementById('loader-text').textContent = 'Encrypting and building PDF...';
const doc = new PDFDocument({
size: [pageImages[0].width, pageImages[0].height],
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
userPassword: password,
ownerPassword: password,
permissions: {
printing: 'highResolution',
modifying: false,
copying: false,
annotating: false,
fillingForms: false,
contentAccessibility: true,
documentAssembly: false
}
});
const stream = doc.pipe(blobStream());
for (let i = 0; i < pageImages.length; i++) {
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
}
doc.end();
stream.on('finish', function () {
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.');
}
}
+61
View File
@@ -0,0 +1,61 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function extractPages() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageInput = document.getElementById('pages-to-extract').value;
if (!pageInput.trim()) {
showAlert('Invalid Input', 'Please enter page numbers to extract.');
return;
}
showLoader('Extracting pages...');
try {
const totalPages = state.pdfDoc.getPageCount();
const indicesToExtract = new Set();
const ranges = pageInput.split(',');
for (const range of ranges) {
const trimmedRange = range.trim();
if (trimmedRange.includes('-')) {
const [start, end] = trimmedRange.split('-').map(Number);
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToExtract.add(pageNum - 1);
}
}
if (indicesToExtract.size === 0) {
showAlert('Invalid Input', 'No valid pages selected for extraction.');
hideLoader();
return;
}
const zip = new JSZip();
// @ts-expect-error TS(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
const sortedIndices = Array.from(indicesToExtract).sort((a, b) => a - b);
for (const index of sortedIndices) {
const newPdf = await PDFLibDocument.create();
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
newPdf.addPage(copiedPage);
const newPdfBytes = await newPdf.save();
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
zip.file(`page-${index + 1}.pdf`, newPdfBytes);
}
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'extracted-pages.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not extract pages.');
} finally {
hideLoader();
}
}
+88
View File
@@ -0,0 +1,88 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
export function setupFixDimensionsUI() {
const targetSizeSelect = document.getElementById('target-size');
const customSizeWrapper = document.getElementById('custom-size-wrapper');
if (targetSizeSelect && customSizeWrapper) {
targetSizeSelect.addEventListener('change', () => {
// @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('hidden', targetSizeSelect.value !== 'Custom');
});
}
}
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 = document.getElementById('target-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 orientation = document.getElementById('orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const scalingMode = document.querySelector('input[name="scaling-mode"]:checked').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...');
try {
let targetWidth, targetHeight;
if (targetSizeKey === 'Custom') {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const width = parseFloat(document.getElementById('custom-width').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const height = parseFloat(document.getElementById('custom-height').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const units = document.getElementById('custom-units').value;
if (units === 'in') {
targetWidth = width * 72;
targetHeight = height * 72;
} else { // mm
targetWidth = width * (72 / 25.4);
targetHeight = height * (72 / 25.4);
}
} else {
[targetWidth, targetHeight] = PageSizes[targetSizeKey];
}
if (orientation === 'landscape' && targetWidth < targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
} else if (orientation === 'portrait' && targetWidth > targetHeight) {
[targetWidth, targetHeight] = [targetHeight, targetWidth];
}
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
for (const sourcePage of sourceDoc.getPages()) {
const { width: sourceWidth, height: sourceHeight } = sourcePage.getSize();
const embeddedPage = await newDoc.embedPage(sourcePage);
const newPage = newDoc.addPage([targetWidth, targetHeight]);
newPage.drawRectangle({ x: 0, y: 0, width: targetWidth, height: targetHeight, color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b) });
const scaleX = targetWidth / sourceWidth;
const scaleY = targetHeight / sourceHeight;
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
const scaledWidth = sourceWidth * scale;
const scaledHeight = sourceHeight * scale;
const x = (targetWidth - scaledWidth) / 2;
const y = (targetHeight - scaledHeight) / 2;
newPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
}
const newPdfBytes = await newDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'standardized.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while standardizing pages.');
} finally {
hideLoader();
}
}
+27
View File
@@ -0,0 +1,27 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
export async function flatten() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
showLoader('Flattening PDF...');
try {
const form = state.pdfDoc.getForm();
form.flatten();
const flattenedBytes = await state.pdfDoc.save();
downloadFile(new Blob([flattenedBytes], { type: 'application/pdf' }), 'flattened.pdf');
} catch (e) {
console.error(e);
if (e.message.includes('getForm')) {
showAlert('No Form Found', 'This PDF does not contain any form fields to flatten.');
} else {
showAlert('Error', 'Could not flatten the PDF.');
}
} finally {
hideLoader();
}
}
+374
View File
@@ -0,0 +1,374 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import {
PDFDocument as PDFLibDocument,
PDFTextField,
PDFCheckBox,
PDFRadioGroup,
PDFDropdown,
PDFButton,
PDFSignature,
PDFOptionList
} from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
let pdfJsDoc: any = null;
let currentPageNum = 1;
let pdfRendering = false;
let renderTimeout: any = null;
const formState = {
scale: 2,
fields: [],
};
let fieldValues: Record<string, any> = {};
async function renderPage() {
if (pdfRendering || !pdfJsDoc) return;
pdfRendering = true;
showLoader(`Rendering page ${currentPageNum}...`);
const page = await pdfJsDoc.getPage(currentPageNum);
const viewport = page.getViewport({ scale: 1.0 });
const canvas = document.getElementById('pdf-canvas') as HTMLCanvasElement;
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;
pdfRendering = false;
hideLoader();
}
async function changePage(offset: number) {
const newPageNum = currentPageNum + offset;
if (newPageNum > 0 && newPageNum <= pdfJsDoc.numPages) {
currentPageNum = newPageNum;
await renderPage();
}
}
async function setZoom(factor: number) {
formState.scale = factor;
await renderPage();
}
function handleFormChange(event: Event) {
const input = event.target as HTMLInputElement | HTMLSelectElement;
const name = input.name;
let value: any;
if (input instanceof HTMLInputElement && input.type === 'checkbox') {
value = input.checked ? 'on' : 'off';
} else if (input instanceof HTMLSelectElement && input.multiple) {
value = Array.from(input.options)
.filter(option => option.selected)
.map(option => option.value);
} else {
value = input.value;
}
fieldValues[name] = value;
clearTimeout(renderTimeout);
renderTimeout = setTimeout(() => {
renderPage();
}, 350);
}
function createFormFieldHtml(field: any): HTMLElement {
const name = field.getName();
const isRequired = field.isRequired();
const labelText = name.replace(/[_-]/g, ' ');
const wrapper = document.createElement('div');
wrapper.className = 'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
const label = document.createElement('label');
label.htmlFor = `field-${name}`;
label.className = 'block text-sm font-medium text-gray-300 capitalize mb-1';
label.textContent = labelText;
if (isRequired) {
const requiredSpan = document.createElement('span');
requiredSpan.className = 'text-red-500';
requiredSpan.textContent = ' *';
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');
}
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 {
const unsupportedDiv = document.createElement('div');
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;
}
wrapper.appendChild(inputElement);
return wrapper;
}
export async function setupFormFiller() {
if (!state.pdfDoc) return;
showLoader('Analyzing form fields...');
const formContainer = document.getElementById('form-fields-container');
const processBtn = document.getElementById('process-btn');
if (!formContainer || !processBtn) {
console.error('Required DOM elements not found');
hideLoader();
return;
}
try {
const form = state.pdfDoc.getForm();
const fields = form.getFields();
formState.fields = fields;
formContainer.textContent = '';
if (fields.length === 0) {
formContainer.innerHTML = '<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
processBtn.classList.add('hidden');
} else {
fields.forEach((field: any) => {
try {
const fieldElement = createFormFieldHtml(field);
formContainer.appendChild(fieldElement);
} catch (e: any) {
console.error(`Error processing field "${field.getName()}":`, e);
const errorDiv = document.createElement('div');
errorDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
// Sanitize error message display
const p1 = document.createElement('p');
p1.className = 'text-sm text-gray-500';
p1.textContent = `Unsupported field: ${field.getName()}`;
const p2 = document.createElement('p');
p2.className = 'text-xs text-gray-500';
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();
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() {
showLoader('Applying form data...');
try {
const form = state.pdfDoc.getForm();
Object.keys(fieldValues).forEach(fieldName => {
try {
const field = form.getField(fieldName);
const value = fieldValues[fieldName];
if (field instanceof PDFTextField) {
field.setText(value);
} else if (field instanceof PDFCheckBox) {
if (value === 'on') {
field.check();
} else {
field.uncheck();
}
} else if (field instanceof PDFRadioGroup) {
field.select(value);
} else if (field instanceof PDFDropdown) {
field.select(value);
} else if (field instanceof PDFOptionList) {
if (Array.isArray(value)) {
value.forEach(option => field.select(option));
}
}
} catch (e) {
console.error(`Error processing field "${fieldName}" during download:`, e);
}
});
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'filled-form.pdf');
showAlert('Success', 'Form has been filled and downloaded.');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to save the filled form.');
} finally {
hideLoader();
}
}
+36
View File
@@ -0,0 +1,36 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import heic2any from 'heic2any';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function heicToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one HEIC file.');
return;
}
showLoader('Converting HEIC to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const conversionResult = await heic2any({
blob: file,
toType: "image/png",
});
const pngBlob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult;
const pngBytes = await pngBlob.arrayBuffer();
const pngImage = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_heic.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.');
} finally {
hideLoader();
}
}
+133
View File
@@ -0,0 +1,133 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
/**
* Converts any image into a standard, web-friendly JPEG. Loses transparency.
* @param {Uint8Array} imageBytes The raw bytes of the image file.
* @returns {Promise<Uint8Array>} A promise that resolves with sanitized JPEG bytes.
*/
function sanitizeImageAsJpeg(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (jpegBlob) => {
if (!jpegBlob) return reject(new Error('Canvas to JPEG conversion failed.'));
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
}, 'image/jpeg', 0.9
);
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(new Error('File could not be loaded as an image.'));
};
img.src = imageUrl;
});
}
/**
* Converts any image into a standard PNG. Preserves transparency.
* @param {Uint8Array} imageBytes The raw bytes of the image file.
* @returns {Promise<Uint8Array>} A promise that resolves with sanitized PNG bytes.
*/
function sanitizeImageAsPng(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (pngBlob) => {
if (!pngBlob) return reject(new Error('Canvas to PNG conversion failed.'));
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
}, 'image/png'
);
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(new Error('File could not be loaded as an image.'));
};
img.src = imageUrl;
});
}
export async function imageToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one image file.');
return;
}
showLoader('Converting images to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
const imageList = document.getElementById('image-list');
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
.map(li => state.files.find(f => f.name === li.dataset.fileName))
.filter(Boolean);
for (const file of sortedFiles) {
const fileBuffer = await readFileAsArrayBuffer(file);
let image;
if (file.type === 'image/jpeg') {
try {
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
} catch (e) {
console.warn(`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`);
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
}
} else if (file.type === 'image/png') {
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 });
}
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();
}
}
+112
View File
@@ -0,0 +1,112 @@
import { merge, setupMergeTool } from './merge.js';
import { setupSplitTool, split } from './split.js';
import { encrypt } from './encrypt.js';
import { decrypt } from './decrypt.js';
import { organize } from './organize.js';
import { rotate } from './rotate.js';
import { addPageNumbers } from './add-page-numbers.js';
import { pdfToJpg } from './pdf-to-jpg.js';
import { jpgToPdf } from './jpg-to-pdf.js';
import { scanToPdf } from './scan-to-pdf.js';
import { compress } from './compress.js';
import { pdfToGreyscale } from './pdf-to-greyscale.js';
import { pdfToZip } from './pdf-to-zip.js';
import { editMetadata } from './edit-metadata.js';
import { removeMetadata } from './remove-metadata.js';
import { flatten } from './flatten.js';
import { pdfToPng } from './pdf-to-png.js';
import { pngToPdf } from './png-to-pdf.js';
import { pdfToWebp } from './pdf-to-webp.js';
import { webpToPdf } from './webp-to-pdf.js';
import { deletePages } from './delete-pages.js';
import { addBlankPage } from './add-blank-page.js';
import { extractPages } from './extract-pages.js';
import { addWatermark, setupWatermarkUI } from './add-watermark.js';
import { addHeaderFooter } from './add-header-footer.js';
import { imageToPdf } from './image-to-pdf.js';
import { changePermissions } from './change-permissions.js';
import { pdfToMarkdown } from './pdf-to-markdown.js';
import { txtToPdf } from './txt-to-pdf.js';
import { invertColors } from './invert-colors.js';
// import { viewMetadata } from './view-metadata.js';
import { reversePages } from './reverse-pages.js';
import { mdToPdf } from './md-to-pdf.js';
import { svgToPdf } from './svg-to-pdf.js';
import { bmpToPdf } from './bmp-to-pdf.js';
import { heicToPdf } from './heic-to-pdf.js';
import { tiffToPdf } from './tiff-to-pdf.js';
import { pdfToBmp } from './pdf-to-bmp.js';
import { pdfToTiff } from './pdf-to-tiff.js';
import { splitInHalf } from './split-in-half.js';
import { analyzeAndDisplayDimensions } from './page-dimensions.js';
import { nUpTool, setupNUpUI } from './n-up.js';
import { processAndSave } from './duplicate-organize.js';
import { combineToSinglePage } from './combine-single-page.js';
import { fixDimensions, setupFixDimensionsUI } from './fix-dimensions.js';
import { changeBackgroundColor } from './change-background-color.js';
import { changeTextColor, setupTextColorTool } from './change-text-color.js';
import { setupCompareTool } from './compare-pdfs.js';
import { setupOcrTool } from './ocr-pdf.js';
import { wordToPdf } from './word-to-pdf.js';
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
import { removeAnnotations, setupRemoveAnnotationsTool } from './remove-annotations.js';
import { setupCropperTool } from './cropper.js';
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
import { posterize, setupPosterizeTool } from './posterize.js';
export const toolLogic = {
merge: { process: merge, setup: setupMergeTool },
split: { process: split, setup: setupSplitTool },
encrypt,
decrypt,
organize,
rotate,
'add-page-numbers': addPageNumbers,
'pdf-to-jpg': pdfToJpg,
'jpg-to-pdf': jpgToPdf,
'scan-to-pdf': scanToPdf,
compress,
'pdf-to-greyscale': pdfToGreyscale,
'pdf-to-zip': pdfToZip,
'edit-metadata': editMetadata,
'remove-metadata': removeMetadata,
flatten,
'pdf-to-png': pdfToPng,
'png-to-pdf': pngToPdf,
'pdf-to-webp': pdfToWebp,
'webp-to-pdf': webpToPdf,
'delete-pages': deletePages,
'add-blank-page': addBlankPage,
'extract-pages': extractPages,
'add-watermark': { process: addWatermark, setup: setupWatermarkUI },
'add-header-footer': addHeaderFooter,
'image-to-pdf': imageToPdf,
'change-permissions': changePermissions,
'pdf-to-markdown': pdfToMarkdown,
'txt-to-pdf': txtToPdf,
'invert-colors': invertColors,
'reverse-pages': reversePages,
'md-to-pdf': mdToPdf,
'svg-to-pdf': svgToPdf,
'bmp-to-pdf': bmpToPdf,
'heic-to-pdf': heicToPdf,
'tiff-to-pdf': tiffToPdf,
'pdf-to-bmp': pdfToBmp,
'pdf-to-tiff': pdfToTiff,
'split-in-half': splitInHalf,
'page-dimensions': analyzeAndDisplayDimensions,
'n-up': { process: nUpTool, setup: setupNUpUI },
'duplicate-organize': { process: processAndSave },
'combine-single-page': combineToSinglePage,
'fix-dimensions': { process: fixDimensions, setup: setupFixDimensionsUI },
'change-background-color': changeBackgroundColor,
'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
'compare-pdfs': { setup: setupCompareTool },
'ocr-pdf': { setup: setupOcrTool },
'word-to-pdf': wordToPdf,
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
'remove-annotations': { process: removeAnnotations, setup: setupRemoveAnnotationsTool },
'cropper': { setup: setupCropperTool },
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller},
'posterize': { process: posterize, setup: setupPosterizeTool },
};
+52
View File
@@ -0,0 +1,52 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function invertColors() {
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
showLoader('Inverting PDF colors...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
// @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++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
data[j] = 255 - data[j]; // red
data[j + 1] = 255 - data[j + 1]; // green
data[j + 2] = 255 - data[j + 2]; // blue
}
ctx.putImageData(imageData, 0, 0);
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png'));
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
const newPage = newPdfDoc.addPage([image.width, image.height]);
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'inverted.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not invert PDF colors.');
} finally {
hideLoader();
}
}
+93
View File
@@ -0,0 +1,93 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
/**
* Takes any image byte array and uses the browser's canvas to convert it
* into a standard, web-friendly (baseline, sRGB) JPEG byte array.
* @param {Uint8Array} imageBytes The raw bytes of the image file.
* @returns {Promise<Uint8Array>} A promise that resolves with the sanitized JPEG bytes.
*/
function sanitizeImageAsJpeg(imageBytes: any) {
return new Promise((resolve, reject) => {
const blob = new Blob([imageBytes]);
const imageUrl = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
canvas.toBlob(
async (jpegBlob) => {
if (!jpegBlob) {
return reject(new Error('Canvas toBlob conversion failed.'));
}
const arrayBuffer = await jpegBlob.arrayBuffer();
resolve(new Uint8Array(arrayBuffer));
},
'image/jpeg',
0.9
);
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
URL.revokeObjectURL(imageUrl);
reject(new Error('The provided file could not be loaded as an image. It may be corrupted.'));
};
img.src = imageUrl;
});
}
export async function jpgToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one JPG file.');
return;
}
showLoader('Creating PDF from JPGs...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const originalBytes = await readFileAsArrayBuffer(file);
let jpgImage;
try {
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
} catch (e) {
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
showAlert(`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`);
try {
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
} catch (fallbackError) {
console.error(`Failed to process ${file.name} after sanitization:`, fallbackError);
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
}
}
const page = pdfDoc.addPage([jpgImage.width, jpgImage.height]);
page.drawImage(jpgImage, {
x: 0,
y: 0,
width: jpgImage.width,
height: jpgImage.height,
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_jpgs.pdf');
} catch (e) {
console.error(e);
showAlert('Conversion Error', e.message);
} finally {
hideLoader();
}
}
+85
View File
@@ -0,0 +1,85 @@
// note: this is a work in progress
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
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...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
const htmlContent = marked.parse(markdownContent);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
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
const orientation = document.getElementById('orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const marginSize = document.getElementById('margin-size').value;
const tempContainer = document.createElement('div');
tempContainer.style.cssText = 'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
const styleSheet = document.createElement('style');
styleSheet.textContent = `
body { font-family: Helvetica, Arial, sans-serif; line-height: 1.6; font-size: 12px; }
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 });
document.body.removeChild(tempContainer);
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
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;
let heightLeft = imgHeight;
let position = margin;
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
heightLeft -= contentHeight;
while (heightLeft > 0) {
position = position - pageHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
heightLeft -= contentHeight;
}
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();
}
}
+396
View File
@@ -0,0 +1,396 @@
import { showLoader, hideLoader, showAlert } from '../ui.ts';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.ts';
import { state } from '../state.ts';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import Sortable from 'sortablejs';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
const mergeState = {
pdfDocs: {},
activeMode: 'file',
sortableInstances: {},
isRendering: false,
cachedThumbnails: null,
lastFileHash: null
};
function parsePageRanges(rangeString: any, totalPages: any) {
const indices = new Set();
if (!rangeString.trim()) return [];
const ranges = rangeString.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++) {
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);
}
function initializeFileListSortable() {
const fileList = document.getElementById('file-list');
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 '{}'.
mergeState.sortableInstances.fileList.destroy();
}
// @ts-expect-error TS(2339) FIXME: Property 'fileList' does not exist on type '{}'.
mergeState.sortableInstances.fileList = Sortable.create(fileList, {
handle: '.drag-handle',
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onStart: function (evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function (evt: any) {
evt.item.style.opacity = '1';
}
});
}
function initializePageThumbnailsSortable() {
const container = document.getElementById('page-merge-preview');
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
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 = Sortable.create(container, {
animation: 150,
ghostClass: 'sortable-ghost',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-drag',
onStart: function (evt: any) {
evt.item.style.opacity = '0.5';
},
onEnd: function (evt: any) {
evt.item.style.opacity = '1';
}
});
}
function generateFileHash() {
return (state.files as File[]).map(f => `${f.name}-${f.size}-${f.lastModified}`).join('|');
}
async function renderPageMergeThumbnails() {
const container = document.getElementById('page-merge-preview');
if (!container) return;
const currentFileHash = generateFileHash();
const filesChanged = currentFileHash !== mergeState.lastFileHash;
if (!filesChanged && mergeState.cachedThumbnails !== null) {
// Simple check to see if it's already rendered to avoid flicker.
if (container.firstChild) {
initializePageThumbnailsSortable();
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();
}
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() {
showLoader('Merging PDFs...');
try {
const newPdfDoc = await PDFLibDocument.create();
if (mergeState.activeMode === 'file') {
const fileList = document.getElementById('file-list');
const sortedFiles = Array.from(fileList.children).map(li => {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
return state.files.find(f => f.name === li.dataset.fileName);
}).filter(Boolean);
for (const file of sortedFiles) {
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
const rangeInput = document.getElementById(`range-${safeFileName}`);
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
const rangeInputValue = rangeInput.value;
const sourcePdf = mergeState.pdfDocs[file.name];
if (!sourcePdf) continue;
const totalPages = sourcePdf.getPageCount();
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
const indicesToCopy = pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
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);
for (const el of pageElements) {
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
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);
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();
}
}
export async function setupMergeTool() {
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
document.getElementById('process-btn').disabled = false;
const wasInPageMode = mergeState.activeMode === 'page';
showLoader('Loading PDF documents...');
try {
for (const file of state.files) {
if (!mergeState.pdfDocs[file.name]) {
const pdfBytes = await readFileAsArrayBuffer(file);
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(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();
}
const fileModeBtn = document.getElementById('file-mode-btn');
const pageModeBtn = document.getElementById('page-mode-btn');
const filePanel = document.getElementById('file-mode-panel');
const pagePanel = document.getElementById('page-mode-panel');
const fileList = document.getElementById('file-list');
fileList.textContent = ''; // Clear list safely
(state.files as File[]).forEach(f => {
const doc = mergeState.pdfDocs[f.name];
const pageCount = doc ? doc.getPageCount() : 'N/A';
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
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.dataset.fileName = f.name;
const mainDiv = document.createElement('div');
mainDiv.className = 'flex items-center justify-between';
const nameSpan = document.createElement('span');
nameSpan.className = 'truncate font-medium text-white flex-1 mr-2';
nameSpan.title = f.name;
nameSpan.textContent = f.name;
const dragHandle = document.createElement('div');
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
mainDiv.append(nameSpan, dragHandle);
const rangeDiv = document.createElement('div');
rangeDiv.className = 'mt-2';
const label = document.createElement('label');
label.htmlFor = `range-${safeFileName}`;
label.className = 'text-xs text-gray-400';
label.textContent = `Pages (e.g., 1-3, 5) - Total: ${pageCount}`;
const input = document.createElement('input');
input.type = 'text';
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.placeholder = 'Leave blank for all pages';
rangeDiv.append(label, input);
li.append(mainDiv, rangeDiv);
fileList.appendChild(li);
});
initializeFileListSortable();
const newFileModeBtn = fileModeBtn.cloneNode(true);
const newPageModeBtn = pageModeBtn.cloneNode(true);
fileModeBtn.replaceWith(newFileModeBtn);
pageModeBtn.replaceWith(newPageModeBtn);
newFileModeBtn.addEventListener('click', () => {
if (mergeState.activeMode === 'file') return;
mergeState.activeMode = 'file';
filePanel.classList.remove('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
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
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
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
newPageModeBtn.classList.add('bg-gray-700', 'text-gray-300');
});
newPageModeBtn.addEventListener('click', async () => {
if (mergeState.activeMode === 'page') return;
mergeState.activeMode = 'page';
filePanel.classList.add('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
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
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
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
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
await renderPageMergeThumbnails();
});
if (wasInPageMode) {
mergeState.activeMode = 'page';
filePanel.classList.add('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
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
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
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
newFileModeBtn.classList.add('bg-gray-700', 'text-gray-300');
await renderPageMergeThumbnails();
} else {
// @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');
// @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');
}
}
+111
View File
@@ -0,0 +1,111 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb, PageSizes } from 'pdf-lib';
export function setupNUpUI() {
const addBorderCheckbox = document.getElementById('add-border');
const borderColorWrapper = document.getElementById('border-color-wrapper');
if (addBorderCheckbox && borderColorWrapper) {
addBorderCheckbox.addEventListener('change', () => {
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
borderColorWrapper.classList.toggle('hidden', !addBorderCheckbox.checked);
});
}
}
export async function nUpTool() {
// 1. Gather all options from the UI
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const n = parseInt(document.getElementById('pages-per-sheet').value);
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const pageSizeKey = document.getElementById('output-page-size').value;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
let orientation = document.getElementById('output-orientation').value;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const useMargins = document.getElementById('add-margins').checked;
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const addBorder = document.getElementById('add-border').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const borderColor = hexToRgb(document.getElementById('border-color').value);
showLoader('Creating N-Up PDF...');
try {
const sourceDoc = state.pdfDoc;
const newDoc = await PDFLibDocument.create();
const sourcePages = sourceDoc.getPages();
const gridDims = { 2: [2, 1], 4: [2, 2], 9: [3, 3], 16: [4, 4] }[n];
let [pageWidth, pageHeight] = PageSizes[pageSizeKey];
if (orientation === 'auto') {
const firstPage = sourcePages[0];
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
orientation = (isSourceLandscape && gridDims[0] > gridDims[1]) ? 'landscape' : 'portrait';
}
if (orientation === 'landscape' && pageWidth < pageHeight) {
[pageWidth, pageHeight] = [pageHeight, pageWidth];
}
const margin = useMargins ? 36 : 0;
const gutter = useMargins ? 10 : 0;
const usableWidth = pageWidth - (margin * 2);
const usableHeight = pageHeight - (margin * 2);
// Loop through the source pages in chunks of 'n'
for (let i = 0; i < sourcePages.length; i += n) {
showLoader(`Processing sheet ${Math.floor(i / n) + 1}...`);
const chunk = sourcePages.slice(i, i + n);
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
// Calculate dimensions of each cell in the grid
const cellWidth = (usableWidth - (gutter * (gridDims[0] - 1))) / gridDims[0];
const cellHeight = (usableHeight - (gutter * (gridDims[1] - 1))) / gridDims[1];
for (let j = 0; j < chunk.length; j++) {
const sourcePage = chunk[j];
const embeddedPage = await newDoc.embedPage(sourcePage);
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
const scale = Math.min(cellWidth / embeddedPage.width, cellHeight / embeddedPage.height);
const scaledWidth = embeddedPage.width * scale;
const scaledHeight = embeddedPage.height * scale;
// Calculate position (x, y) for this cell
const row = Math.floor(j / gridDims[0]);
const col = j % gridDims[0];
const cellX = margin + col * (cellWidth + gutter);
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
// Center the page within its cell
const x = cellX + (cellWidth - scaledWidth) / 2;
const y = cellY + (cellHeight - scaledHeight) / 2;
outputPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
if (addBorder) {
outputPage.drawRectangle({
x, y,
width: scaledWidth,
height: scaledHeight,
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
borderWidth: 1,
});
}
}
}
const newPdfBytes = await newDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), `n-up_${n}.pdf`);
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
} finally {
hideLoader();
}
}
+278
View File
@@ -0,0 +1,278 @@
import { tesseractLanguages } from '../config/tesseract-languages.js';
import { showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import Tesseract from 'tesseract.js';
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
import { icons, createIcons } from "lucide";
let searchablePdfBytes: any = null;
function sanitizeTextForWinAnsi(text: string): string {
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
return text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
}
function parseHOCR(hocrText: string) {
const parser = new DOMParser();
const doc = parser.parseFromString(hocrText, 'text/html');
const words = [];
// Find all word elements in hOCR
const wordElements = doc.querySelectorAll('.ocrx_word');
wordElements.forEach((wordEl) => {
const titleAttr = wordEl.getAttribute('title');
const text = wordEl.textContent?.trim() || '';
if (!titleAttr || !text) return;
// Parse bbox coordinates from title attribute
// Format: "bbox x0 y0 x1 y1; x_wconf confidence"
const bboxMatch = titleAttr.match(/bbox (\d+) (\d+) (\d+) (\d+)/);
const confMatch = titleAttr.match(/x_wconf (\d+)/);
if (bboxMatch) {
words.push({
text: text,
bbox: {
x0: parseInt(bboxMatch[1]),
y0: parseInt(bboxMatch[2]),
x1: parseInt(bboxMatch[3]),
y1: parseInt(bboxMatch[4])
},
confidence: confMatch ? parseInt(confMatch[1]) : 0
});
}
});
return words;
}
function binarizeCanvas(ctx: any) {
const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
// A simple luminance-based threshold for determining black or white
const brightness = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
data[i] = data[i + 1] = data[i + 2] = color;
}
ctx.putImageData(imageData, 0, 0);
}
function updateProgress(status: any, progress: any) {
const progressBar = document.getElementById('progress-bar');
const progressStatus = document.getElementById('progress-status');
const progressLog = document.getElementById('progress-log');
if (!progressBar || !progressStatus || !progressLog) return;
progressStatus.textContent = status;
// Tesseract's progress can sometimes exceed 1, so we cap it at 100%.
progressBar.style.width = `${Math.min(100, progress * 100)}%`;
const logMessage = `Status: ${status}`;
progressLog.textContent += logMessage + '\n';
progressLog.scrollTop = progressLog.scrollHeight;
}
async function runOCR() {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const selectedLangs = Array.from(document.querySelectorAll('.lang-checkbox:checked')).map(cb => cb.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 scale = parseFloat(document.getElementById('ocr-resolution').value);
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
const binarize = document.getElementById('ocr-binarize').checked;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const whitelist = document.getElementById('ocr-whitelist').value;
if (selectedLangs.length === 0) {
showAlert('No Languages Selected', 'Please select at least one language for OCR.');
return;
}
const langString = selectedLangs.join('+');
document.getElementById('ocr-options').classList.add('hidden');
document.getElementById('ocr-progress').classList.remove('hidden');
try {
const worker = await Tesseract.createWorker(langString, 1, {
logger: (m: any) => updateProgress(m.status, m.progress || 0)
});
// Enable hOCR output
await worker.setParameters({
tessjs_create_hocr: '1',
});
if (whitelist.trim()) {
await worker.setParameters({
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);
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);
}
});
}
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
document.getElementById('ocr-text-output').value = fullText.trim();
document.getElementById('download-searchable-pdf').addEventListener('click', () => {
downloadFile(new Blob([searchablePdfBytes], { type: 'application/pdf' }), 'searchable.pdf');
});
// CHANGE: The copy button logic is updated to be safer.
document.getElementById('copy-text-btn').addEventListener('click', (e) => {
const button = e.currentTarget as HTMLButtonElement;
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme...
const textToCopy = document.getElementById('ocr-text-output').value;
navigator.clipboard.writeText(textToCopy).then(() => {
button.textContent = ''; // Clear the button safely
const icon = document.createElement('i');
icon.setAttribute('data-lucide', 'check');
icon.className = 'w-4 h-4 text-green-400';
button.appendChild(icon);
createIcons({ icons });
setTimeout(() => {
const currentButton = document.getElementById('copy-text-btn');
if (currentButton) {
currentButton.textContent = ''; // Clear the button safely
const resetIcon = document.createElement('i');
resetIcon.setAttribute('data-lucide', 'clipboard-copy');
resetIcon.className = 'w-4 h-4 text-gray-300';
currentButton.appendChild(resetIcon);
createIcons({ icons });
}
}, 2000);
});
});
document.getElementById('download-txt-btn').addEventListener('click', () => {
// @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.
*/
export function setupOcrTool() {
const langSearch = document.getElementById('lang-search');
const langList = document.getElementById('lang-list');
const selectedLangsDisplay = document.getElementById('selected-langs-display');
const processBtn = document.getElementById('process-btn');
langSearch.addEventListener('input', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const searchTerm = langSearch.value.toLowerCase();
langList.querySelectorAll('label').forEach(label => {
label.style.display = label.textContent.toLowerCase().includes(searchTerm) ? '' : 'none';
});
});
// Update the display of selected languages
langList.addEventListener('change', () => {
const selected = Array.from(langList.querySelectorAll('.lang-checkbox:checked'))
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
.map(cb => tesseractLanguages[cb.value]);
selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None';
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
processBtn.disabled = selected.length === 0;
});
// Attach the main OCR function to the process button
processBtn.addEventListener('click', runOCR);
}
+27
View File
@@ -0,0 +1,27 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function organize() {
showLoader('Saving changes...');
try {
const newPdf = await PDFLibDocument.create();
const pageContainer = document.getElementById('page-organizer');
// @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 => parseInt(child.dataset.pageIndex));
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not save the changes.');
} finally {
hideLoader();
}
}
+80
View File
@@ -0,0 +1,80 @@
import { state } from '../state.js';
import { getStandardPageName, convertPoints } from '../utils/helpers.js';
let analyzedPagesData: any = []; // Store raw data to avoid re-analyzing
/**
* Renders the dimensions table based on the stored data and selected unit.
* @param {string} unit The unit to display dimensions in ('pt', 'in', 'mm', 'px').
*/
function renderTable(unit: any) {
const tableBody = document.getElementById('dimensions-table-body');
if (!tableBody) return;
tableBody.textContent = ''; // Clear the table body safely
analyzedPagesData.forEach(pageData => {
const width = convertPoints(pageData.width, unit);
const height = convertPoints(pageData.height, unit);
const row = document.createElement('tr');
// Create and append each cell safely using textContent
const pageNumCell = document.createElement('td');
pageNumCell.className = 'px-4 py-3 text-white';
pageNumCell.textContent = pageData.pageNum;
const dimensionsCell = document.createElement('td');
dimensionsCell.className = 'px-4 py-3 text-gray-300';
dimensionsCell.textContent = `${width} x ${height} ${unit}`;
const sizeCell = document.createElement('td');
sizeCell.className = 'px-4 py-3 text-gray-300';
sizeCell.textContent = pageData.standardSize;
const orientationCell = document.createElement('td');
orientationCell.className = 'px-4 py-3 text-gray-300';
orientationCell.textContent = pageData.orientation;
row.append(pageNumCell, dimensionsCell, sizeCell, orientationCell);
tableBody.appendChild(row);
});
}
/**
* Main function to analyze the PDF and display dimensions.
* This is called once after the file is loaded.
*/
export function analyzeAndDisplayDimensions() {
if (!state.pdfDoc) return;
analyzedPagesData = []; // Reset stored data
const pages = state.pdfDoc.getPages();
pages.forEach((page: any, index: any) => {
const { width, height } = page.getSize();
analyzedPagesData.push({
pageNum: index + 1,
width, // Store raw width in points
height, // Store raw height in points
orientation: width > height ? 'Landscape' : 'Portrait',
standardSize: getStandardPageName(width, height),
});
});
const resultsContainer = document.getElementById('dimensions-results');
const unitsSelect = document.getElementById('units-select');
// Initial render with default unit (points)
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
renderTable(unitsSelect.value);
// Show the results table
resultsContainer.classList.remove('hidden');
// Add event listener to handle unit changes
unitsSelect.addEventListener('change', (e) => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
renderTable(e.target.value);
});
}
+93
View File
@@ -0,0 +1,93 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
/**
* Creates a BMP file buffer from raw pixel data (ImageData).
* This function is self-contained and has no external dependencies.
* @param {ImageData} imageData The pixel data from a canvas context.
* @returns {ArrayBuffer} The complete BMP file as an ArrayBuffer.
*/
function encodeBMP(imageData: any) {
const { width, height, data } = imageData;
const stride = Math.floor((24 * width + 31) / 32) * 4; // Row size must be a multiple of 4 bytes
const fileSize = stride * height + 54; // 54 byte header
const buffer = new ArrayBuffer(fileSize);
const view = new DataView(buffer);
// BMP File Header (14 bytes)
view.setUint16(0, 0x4D42, true); // 'BM'
view.setUint32(2, fileSize, true);
view.setUint32(10, 54, true); // Offset to pixel data
// DIB Header (BITMAPINFOHEADER) (40 bytes)
view.setUint32(14, 40, true); // DIB header size
view.setUint32(18, width, true);
view.setUint32(22, -height, true); // Negative height for top-down scanline order
view.setUint16(26, 1, true); // Color planes
view.setUint16(28, 24, true); // Bits per pixel
view.setUint32(30, 0, true); // No compression
view.setUint32(34, stride * height, true); // Image size
view.setUint32(38, 2835, true); // Horizontal resolution (72 DPI)
view.setUint32(42, 2835, true); // Vertical resolution (72 DPI)
// Pixel Data
let offset = 54;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// BMP is BGR, not RGB
view.setUint8(offset++, data[i + 2]); // Blue
view.setUint8(offset++, data[i + 1]); // Green
view.setUint8(offset++, data[i]); // Red
}
// Add padding to make the row a multiple of 4 bytes
for (let p = 0; p < (stride - width * 3); p++) {
view.setUint8(offset++, 0);
}
}
return buffer;
}
export async function pdfToBmp() {
showLoader('Converting PDF to BMP images...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
// Render the PDF page directly to the canvas
await page.render({ canvasContext: context, viewport: viewport }).promise;
// Get the raw pixel data from this canvas
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// Use our new self-contained function to create the BMP file
const bmpBuffer = encodeBMP(imageData);
// Add the generated BMP file to the zip archive
zip.file(`page_${i}.bmp`, bmpBuffer);
}
showLoader('Compressing files into a ZIP...');
const zipBlob = await zip.generateAsync({ type: "blob" });
downloadFile(zipBlob, 'converted_bmp_images.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to BMP. The file might be corrupted.');
} finally {
hideLoader();
}
}
+58
View File
@@ -0,0 +1,58 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function pdfToGreyscale() {
if (!state.pdfDoc) {
showAlert('Error', 'PDF not loaded.');
return;
}
showLoader('Converting to greyscale...');
try {
const newPdfDoc = await PDFLibDocument.create();
const pdfBytes = await state.pdfDoc.save();
// @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++) {
const page = await pdfjsDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.5 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
await page.render({ canvasContext: ctx, viewport }).promise;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (let j = 0; j < data.length; j += 4) {
const avg = (data[j] + data[j + 1] + data[j + 2]) / 3;
data[j] = avg; // red
data[j + 1] = avg; // green
data[j + 2] = avg; // blue
}
ctx.putImageData(imageData, 0, 0);
const imageBytes = await new Promise(resolve => canvas.toBlob(blob => {
const reader = new FileReader();
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
reader.onload = () => resolve(new Uint8Array(reader.result));
reader.readAsArrayBuffer(blob);
}, 'image/png'));
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
const newPage = newPdfDoc.addPage([image.width, image.height]);
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
}
const newPdfBytes = await newPdfDoc.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'greyscale.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not convert to greyscale.');
} finally {
hideLoader();
}
}
+35
View File
@@ -0,0 +1,35 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
export async function pdfToJpg() {
showLoader('Converting to JPG...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise(resolve => 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();
}
}
+31
View File
@@ -0,0 +1,31 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
export async function pdfToMarkdown() {
showLoader('Converting to Markdown...');
try {
const file = state.files[0];
const arrayBuffer = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let markdown = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
// 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(' ');
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();
}
}
+33
View File
@@ -0,0 +1,33 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
export async function pdfToPng() {
showLoader('Converting to PNG...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
zip.file(`page_${i}.png`, blob as Blob);
}
const zipBlob = await zip.generateAsync({ type: "blob" });
downloadFile(zipBlob, 'converted_pngs.zip');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert PDF to PNG.');
} finally {
hideLoader();
}
}
+38
View File
@@ -0,0 +1,38 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import UTIF from 'utif';
import * as pdfjsLib from "pdfjs-dist";
export async function pdfToTiff() {
showLoader('Converting PDF to TIFF...');
try {
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 }); // Use 2x scale for high quality
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;
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);
}
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();
}
}
+32
View File
@@ -0,0 +1,32 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
export async function pdfToWebp() {
showLoader('Converting to WebP...');
try {
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
const zip = new JSZip();
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2.0 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
const context = canvas.getContext('2d');
await page.render({ canvasContext: context, viewport: viewport }).promise;
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 0.9));
zip.file(`page_${i}.webp`, blob as Blob);
}
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();
}
}
+27
View File
@@ -0,0 +1,27 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
export async function pdfToZip() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select one or more PDF files.');
return;
}
showLoader('Creating ZIP file...');
try {
const zip = new JSZip();
for (const file of state.files) {
const fileBuffer = await readFileAsArrayBuffer(file);
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();
}
}
+35
View File
@@ -0,0 +1,35 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function pngToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one PNG file.');
return;
}
showLoader('Creating PDF from PNGs...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await readFileAsArrayBuffer(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngImage.width,
height: pngImage.height,
});
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_pngs.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to create PDF from PNG images. Ensure all files are valid PNGs.');
} finally {
hideLoader();
}
}
+202
View File
@@ -0,0 +1,202 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, parsePageRanges } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument, PageSizes } from 'pdf-lib';
import * as pdfjsLib from 'pdfjs-dist';
import { createIcons, icons } from 'lucide';
const posterizeState = {
pdfJsDoc: null,
pageSnapshots: {},
currentPage: 1,
};
async function renderPosterizePreview(pageNum: number) {
if (!posterizeState.pdfJsDoc) return;
posterizeState.currentPage = pageNum;
showLoader(`Rendering preview for page ${pageNum}...`);
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
if (posterizeState.pageSnapshots[pageNum]) {
canvas.width = posterizeState.pageSnapshots[pageNum].width;
canvas.height = posterizeState.pageSnapshots[pageNum].height;
context.putImageData(posterizeState.pageSnapshots[pageNum], 0, 0);
} else {
const page = await posterizeState.pdfJsDoc.getPage(pageNum);
const viewport = page.getViewport({ scale: 1.5 });
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: context, viewport }).promise;
posterizeState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height);
}
updatePreviewNav();
drawGridOverlay();
hideLoader();
}
function drawGridOverlay() {
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
const canvas = document.getElementById('posterize-preview-canvas') 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 pagesToProcess = parsePageRanges(pageRangeInput, posterizeState.pdfJsDoc.numPages);
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
const rows = 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.lineWidth = 2;
context.setLineDash([10, 5]);
const cellWidth = canvas.width / cols;
const cellHeight = canvas.height / rows;
for (let i = 1; i < cols; i++) {
context.beginPath();
context.moveTo(i * cellWidth, 0);
context.lineTo(i * cellWidth, canvas.height);
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([]);
}
}
function updatePreviewNav() {
const currentPageSpan = document.getElementById('current-preview-page');
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement;
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement;
currentPageSpan.textContent = posterizeState.currentPage.toString();
prevBtn.disabled = posterizeState.currentPage <= 1;
nextBtn.disabled = posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
}
export async function setupPosterizeTool() {
if (state.pdfDoc) {
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
const pdfBytes = await state.pdfDoc.save();
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
posterizeState.pageSnapshots = {};
posterizeState.currentPage = 1;
document.getElementById('total-preview-pages').textContent = posterizeState.pdfJsDoc.numPages.toString();
await renderPosterizePreview(1);
document.getElementById('prev-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage - 1);
document.getElementById('next-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage + 1);
['posterize-rows', 'posterize-cols', 'page-range'].forEach(id => {
document.getElementById(id).addEventListener('input', drawGridOverlay);
});
createIcons({ icons });
}
}
export async function posterize() {
showLoader('Posterizing PDF...');
try {
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
const cols = parseInt((document.getElementById('posterize-cols') 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;
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
else if (overlapUnits === 'mm') overlapInPoints = overlap * (72 / 25.4);
const newDoc = await PDFDocument.create();
const totalPages = posterizeState.pdfJsDoc.numPages;
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
if (pageIndicesToProcess.length === 0) {
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();
}
}
+41
View File
@@ -0,0 +1,41 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
// @ts-expect-error TS(2339) FIXME: Property 'PDFLib' does not exist on type 'Window &... Remove this comment to see the full error message
const { rgb } = window.PDFLib;
export async function redact(redactions: any, canvasScale: any) {
showLoader('Applying redactions...');
try {
const pdfPages = state.pdfDoc.getPages();
const conversionScale = 1 / canvasScale;
redactions.forEach((r: any) => {
const page = pdfPages[r.pageIndex];
const { height: pageHeight } = page.getSize();
// Convert canvas coordinates back to PDF coordinates
const pdfX = r.canvasX * conversionScale;
const pdfWidth = r.canvasWidth * conversionScale;
const pdfHeight = r.canvasHeight * conversionScale;
const pdfY = pageHeight - (r.canvasY * conversionScale) - pdfHeight;
page.drawRectangle({
x: pdfX,
y: pdfY,
width: pdfWidth,
height: pdfHeight,
color: rgb(0, 0, 0),
});
});
const redactedBytes = await state.pdfDoc.save();
downloadFile(new Blob([redactedBytes], { type: 'application/pdf' }), 'redacted.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to apply redactions.');
} finally {
hideLoader();
}
}
+104
View File
@@ -0,0 +1,104 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFName } from 'pdf-lib';
export function setupRemoveAnnotationsTool() {
if (state.pdfDoc) {
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount();
}
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
const pageRangeWrapper = document.getElementById('page-range-wrapper');
pageScopeRadios.forEach(radio => {
radio.addEventListener('change', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
});
});
const selectAllCheckbox = document.getElementById('select-all-annotations');
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
selectAllCheckbox.addEventListener('change', () => {
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
checkbox.checked = selectAllCheckbox.checked;
});
});
}
export async function removeAnnotations() {
showLoader('Removing annotations...');
try {
const totalPages = state.pdfDoc.getPageCount();
let targetPageIndices = [];
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
const pageScope = document.querySelector('input[name="page-scope"]:checked').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 {
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.');
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();
}
}
+32
View File
@@ -0,0 +1,32 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
export async function removeMetadata() {
showLoader('Removing all metadata...');
try {
const infoDict = state.pdfDoc.getInfoDict();
const allKeys = infoDict.keys();
allKeys.forEach((key: any) => {
infoDict.delete(key);
});
state.pdfDoc.setTitle('');
state.pdfDoc.setAuthor('');
state.pdfDoc.setSubject('');
state.pdfDoc.setKeywords([]);
state.pdfDoc.setCreator('');
state.pdfDoc.setProducer('');
const newPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-removed.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'An error occurred while trying to remove metadata.');
} finally {
hideLoader();
}
}
+26
View File
@@ -0,0 +1,26 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function reversePages() {
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
showLoader('Reversing page order...');
try {
const newPdf = await PDFLibDocument.create();
const pageCount = state.pdfDoc.getPageCount();
const reversedIndices = Array.from({ length: pageCount }, (_, i) => pageCount - 1 - i);
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices);
copiedPages.forEach((page: any) => newPdf.addPage(page));
const newPdfBytes = await newPdf.save();
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'reversed.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not reverse the PDF pages.');
} finally {
hideLoader();
}
}
+30
View File
@@ -0,0 +1,30 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { degrees } from 'pdf-lib';
export async function rotate() {
showLoader('Applying rotations...');
try {
const pages = state.pdfDoc.getPages();
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
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
const rotation = parseInt(item.dataset.rotation || '0');
if (rotation !== 0) {
const currentRotation = pages[pageIndex].getRotation().angle;
pages[pageIndex].setRotation(degrees(currentRotation + rotation));
}
});
const rotatedPdfBytes = await state.pdfDoc.save();
downloadFile(new Blob([rotatedPdfBytes], { type: 'application/pdf' }), 'rotated.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Could not apply rotations.');
} finally {
hideLoader();
}
}
+4
View File
@@ -0,0 +1,4 @@
// This is essentially the same as image-to-pdf.
import { imageToPdf } from './image-to-pdf.js';
export const scanToPdf = imageToPdf;
+416
View File
@@ -0,0 +1,416 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import html2canvas from 'html2canvas';
const signState = {
pdf: null, canvas: null, context: null, pageRendering: false,
currentPageNum: 1, scale: 1.0,
pageSnapshot: null,
drawCanvas: null, drawContext: null, isDrawing: false,
savedSignatures: [], placedSignatures: [], activeSignature: null,
interactionMode: 'none',
draggedSigId: null,
dragOffsetX: 0, dragOffsetY: 0,
hoveredSigId: null,
resizeHandle: null,
};
async function renderPage(num: any) {
signState.pageRendering = true;
const page = await signState.pdf.getPage(num);
const viewport = page.getViewport({ scale: signState.scale });
signState.canvas.height = viewport.height;
signState.canvas.width = viewport.width;
await page.render({ canvasContext: signState.context, viewport }).promise;
signState.pageSnapshot = signState.context.getImageData(0, 0, signState.canvas.width, signState.canvas.height);
drawSignatures();
signState.pageRendering = false;
document.getElementById('current-page-display-sign').textContent = num;
}
function drawSignatures() {
if (!signState.pageSnapshot) return;
signState.context.putImageData(signState.pageSnapshot, 0, 0);
signState.placedSignatures
.filter(sig => sig.pageIndex === signState.currentPageNum - 1)
.forEach(sig => {
signState.context.drawImage(sig.image, sig.x, sig.y, sig.width, sig.height);
if (signState.hoveredSigId === sig.id || signState.draggedSigId === sig.id) {
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);
}
});
}
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);
});
}
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() {
signState.drawCanvas = document.getElementById('signature-draw-canvas');
signState.drawContext = signState.drawCanvas.getContext('2d');
const rect = signState.drawCanvas.getBoundingClientRect();
const dpi = window.devicePixelRatio || 1;
signState.drawCanvas.width = rect.width * dpi;
signState.drawCanvas.height = rect.height * dpi;
signState.drawContext.scale(dpi, dpi);
signState.drawContext.lineWidth = 2;
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) => {
signState.isDrawing = true;
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) {
const rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
function addSignatureToSaved(imageDataUrl: any) {
const img = new Image();
img.src = imageDataUrl;
signState.savedSignatures.push(img);
renderSavedSignatures();
}
function renderSavedSignatures() {
const container = document.getElementById('saved-signatures-container');
container.textContent = ''; //change
signState.savedSignatures.forEach((img, index) => {
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';
img.className = 'h-full w-auto mx-auto';
wrapper.appendChild(img);
container.appendChild(wrapper);
wrapper.onclick = () => {
signState.activeSignature = { image: img, index };
document.querySelectorAll('.saved-signature').forEach(el => el.classList.remove('selected'));
wrapper.classList.add('selected');
};
});
}
function getResizeHandles(sig: any) {
return {
'top-left': { x: sig.x, y: sig.y },
'top-middle': { x: sig.x + sig.width / 2, 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-right': { x: sig.x + sig.width, y: sig.y + sig.height / 2 },
'bottom-left': { x: sig.x, 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 },
};
}
function getHandleAtPos(pos: any, sig: any) {
const handles = getResizeHandles(sig);
const handleSize = 10;
for (const [name, handlePos] of Object.entries(handles)) {
if (Math.abs(pos.x - handlePos.x) < handleSize && Math.abs(pos.y - handlePos.y) < handleSize) {
return name;
}
}
return null;
}
function setupPlacementListeners() {
const canvas = signState.canvas;
const ghost = document.getElementById('signature-ghost');
const mouseMoveHandler = (e: any) => {
if (signState.interactionMode !== 'none') return;
if (signState.activeSignature) {
ghost.style.backgroundImage = `url('${signState.activeSignature.image.src}')`;
ghost.style.width = '150px';
ghost.style.height = `${(signState.activeSignature.image.height / signState.activeSignature.image.width) * 150}px`;
ghost.style.left = `${e.clientX + 5}px`;
ghost.style.top = `${e.clientY + 5}px`;
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;
}
});
canvas.className = '';
if (foundHandle) {
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');
}
if (signState.hoveredSigId !== foundSigId) {
signState.hoveredSigId = foundSigId;
drawSignatures();
}
};
canvas.addEventListener('mousemove', mouseMoveHandler);
document.getElementById('canvas-container-sign').addEventListener('mouseleave', () => ghost.classList.add('hidden'));
const onDragStart = (e: any) => {
const pos = getMousePos(canvas, e.touches ? e.touches[0] : e);
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 (clickedOnSignature) {
ghost.classList.add('hidden');
} 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();
}
};
const onDragMove = (e: any) => {
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;
if (signState.interactionMode === 'drag') {
sig.x = pos.x - signState.dragOffsetX;
sig.y = pos.y - signState.dragOffsetY;
} else if (signState.interactionMode === 'resize') {
const originalRight = sig.x + sig.width;
const originalBottom = sig.y + sig.height;
if (signState.resizeHandle.includes('right')) sig.width = Math.max(20, pos.x - sig.x);
if (signState.resizeHandle.includes('bottom')) sig.height = Math.max(20, pos.y - sig.y);
if (signState.resizeHandle.includes('left')) {
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() {
document.getElementById('signature-editor').classList.remove('hidden');
signState.canvas = document.getElementById('canvas-sign');
signState.context = signState.canvas.getContext('2d');
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
signState.pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
document.getElementById('total-pages-display-sign').textContent = signState.pdf.numPages;
await fitToWidth();
setupDrawingCanvas();
setupPlacementListeners();
document.getElementById('prev-page-sign').onclick = () => { if (signState.currentPageNum > 1) { signState.currentPageNum--; renderPage(signState.currentPageNum); } };
document.getElementById('next-page-sign').onclick = () => { if (signState.currentPageNum < signState.pdf.numPages) { signState.currentPageNum++; renderPage(signState.currentPageNum); } };
document.getElementById('zoom-in-btn').onclick = () => { signState.scale += 0.25; renderPage(signState.currentPageNum); };
document.getElementById('zoom-out-btn').onclick = () => { signState.scale = Math.max(0.25, signState.scale - 0.25); renderPage(signState.currentPageNum); };
document.getElementById('fit-width-btn').onclick = fitToWidth;
document.getElementById('undo-btn').onclick = () => { signState.placedSignatures.pop(); drawSignatures(); };
const tabs = ['draw', 'type', 'upload'];
const tabButtons = tabs.map(t => document.getElementById(`${t}-tab-btn`));
const tabPanels = tabs.map(t => document.getElementById(`${t}-panel`));
tabButtons.forEach((button, index) => {
button.onclick = () => {
tabPanels.forEach(panel => panel.classList.add('hidden'));
tabButtons.forEach(btn => {
btn.classList.remove('border-indigo-500', 'text-white');
btn.classList.add('border-transparent', 'text-gray-400');
});
tabPanels[index].classList.remove('hidden');
button.classList.add('border-indigo-500', 'text-white');
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('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() {
if (signState.placedSignatures.length === 0) {
showAlert('No Signatures Placed', 'Please place at least one signature.');
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,
});
}
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();
}
}
+53
View File
@@ -0,0 +1,53 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument, rgb } from 'pdf-lib';
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
const splitType = document.getElementById('split-type').value;
if (!state.pdfDoc) {
showAlert('Error', 'No PDF document is loaded.');
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);
}
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();
}
}
+213
View File
@@ -0,0 +1,213 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import JSZip from 'jszip';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
// Track if visual selector has been rendered to avoid duplicates
let visualSelectorRendered = false;
async function renderVisualSelector() {
if (visualSelectorRendered) return;
const container = document.getElementById('page-selector-grid');
if (!container) return;
visualSelectorRendered = true;
container.textContent = '';
showLoader('Rendering page previews...');
try {
const pdfData = await state.pdfDoc.save();
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 0.4 });
const canvas = document.createElement('canvas');
canvas.height = viewport.height;
canvas.width = viewport.width;
await page.render({ canvasContext: canvas.getContext('2d'), viewport: viewport }).promise;
const wrapper = document.createElement('div');
wrapper.className = 'page-thumbnail-wrapper p-1 border-2 border-transparent rounded-lg cursor-pointer hover:border-indigo-500';
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
wrapper.dataset.pageIndex = i - 1;
const img = document.createElement('img');
img.src = canvas.toDataURL();
img.className = 'rounded-md w-full h-auto';
const p = document.createElement('p');
p.className = 'text-center text-xs mt-1 text-gray-300';
p.textContent = `Page ${i}`;
wrapper.append(img, p);
const handleSelection = (e: any) => {
e.preventDefault();
e.stopPropagation();
const isSelected = wrapper.classList.contains('selected');
if (isSelected) {
wrapper.classList.remove('selected', 'border-indigo-500');
wrapper.classList.add('border-transparent');
} else {
wrapper.classList.add('selected', 'border-indigo-500');
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();
}
}
export function setupSplitTool() {
const splitModeSelect = document.getElementById('split-mode');
const rangePanel = document.getElementById('range-panel');
const visualPanel = document.getElementById('visual-select-panel');
const evenOddPanel = document.getElementById('even-odd-panel');
const zipOptionWrapper = document.getElementById('zip-option-wrapper');
const allPagesPanel = document.getElementById('all-pages-panel');
if (!splitModeSelect) return;
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
const mode = e.target.value;
if (mode !== 'visual') {
visualSelectorRendered = false;
const container = document.getElementById('page-selector-grid');
if (container) container.innerHTML = '';
}
rangePanel.classList.add('hidden');
visualPanel.classList.add('hidden');
evenOddPanel.classList.add('hidden');
allPagesPanel.classList.add('hidden');
zipOptionWrapper.classList.add('hidden');
if (mode === 'range') {
rangePanel.classList.remove('hidden');
zipOptionWrapper.classList.remove('hidden');
} else if (mode === 'visual') {
visualPanel.classList.remove('hidden');
zipOptionWrapper.classList.remove('hidden');
renderVisualSelector();
} else if (mode === 'even-odd') {
evenOddPanel.classList.remove('hidden');
} else if (mode === 'all') {
allPagesPanel.classList.remove('hidden');
}
});
}
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
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 = document.getElementById('download-as-zip')?.checked || false;
showLoader('Splitting PDF...');
try {
const totalPages = state.pdfDoc.getPageCount();
let indicesToExtract: any = [];
switch (splitMode) {
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
const pageRangeInput = document.getElementById('page-range').value;
if (!pageRangeInput) throw new Error('Please enter a page range.');
const ranges = pageRangeInput.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.push(i - 1);
} else {
const pageNum = Number(trimmedRange);
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) continue;
indicesToExtract.push(pageNum - 1);
}
}
break;
case 'even-odd':
const choiceElement = document.querySelector('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;
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;
}
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
View File
@@ -0,0 +1,55 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
async function convertImageToPngBytes(file: any) {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = (e) => {
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
resolve(pngBytes);
};
img.onerror = () => reject(new Error('Failed to load image.'));
// @ts-expect-error TS(2322) FIXME: Type 'string | ArrayBuffer' is not assignable to t... Remove this comment to see the full error message
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Failed to read file.'));
reader.readAsDataURL(file);
});
}
export async function svgToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one SVG file.');
return;
}
showLoader('Converting SVG to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const pngBytes = await convertImageToPngBytes(file);
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
}
const pdfBytes = await pdfDoc.save();
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_svgs.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert SVG to PDF. One of the files may be invalid.');
} finally {
hideLoader();
}
}
+82
View File
@@ -0,0 +1,82 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { decode } from 'tiff';
export async function tiffToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one TIFF file.');
return;
}
showLoader('Converting TIFF to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
const tiffBytes = await readFileAsArrayBuffer(file);
const ifds = decode(tiffBytes as any);
for (const ifd of ifds) {
const canvas = document.createElement('canvas');
canvas.width = ifd.width;
canvas.height = ifd.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
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');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert TIFF to PDF. One of the files may be invalid or corrupted.');
} finally {
hideLoader();
}
}
+74
View File
@@ -0,0 +1,74 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, hexToRgb } from '../utils/helpers.js';
import { PDFDocument as PDFLibDocument, rgb, StandardFonts, PageSizes } from 'pdf-lib';
export async function txtToPdf() {
showLoader('Creating PDF...');
try {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const text = document.getElementById('text-input').value;
if (!text.trim()) {
showAlert('Input Required', 'Please enter some text to convert.');
hideLoader();
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();
}
}
+4
View File
@@ -0,0 +1,4 @@
// This tool doesn't have a "process" button. Its logic is handled directly in fileHandler.js after a file is uploaded.
export function viewMetadata() {
console.log("");
}
+48
View File
@@ -0,0 +1,48 @@
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
export async function webpToPdf() {
if (state.files.length === 0) {
showAlert('No Files', 'Please select at least one WebP file.');
return;
}
showLoader('Converting WebP to PDF...');
try {
const pdfDoc = await PDFLibDocument.create();
for (const file of state.files) {
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
const imageBitmap = await createImageBitmap(new Blob([webpBytes]));
const canvas = document.createElement('canvas');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imageBitmap, 0, 0);
const pngBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
const pngBytes = await pngBlob.arrayBuffer();
// Embed the converted PNG into the PDF
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_webp.pdf');
} catch (e) {
console.error(e);
showAlert('Error', 'Failed to convert WebP to PDF. Ensure all files are valid WebP images.');
} finally {
hideLoader();
}
}
+131
View File
@@ -0,0 +1,131 @@
// NOTE: This is a work in progress and does not work correctly as of yet
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
export async function wordToPdf() {
const file = state.files[0];
if (!file) {
showAlert('No File', 'Please upload a .docx file first.');
return;
}
showLoader('Preparing preview...');
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);
// Get references to our modal elements from index.html
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>
#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 td, #preview-content th { border: 1px solid #dddddd; text-align: left; padding: 8px; }
#preview-content img { max-width: 100%; height: auto; }
#preview-content a { color: #0000ee; text-decoration: underline; }
</style>
${html}
`;
previewContent.innerHTML = styledHtml;
const marginDiv = document.createElement('div');
marginDiv.style.height = '100px';
previewContent.appendChild(marginDiv);
const images = previewContent.querySelectorAll('img');
const imagePromises = Array.from(images).map(img => {
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
if (img.complete) resolve();
else img.onload = resolve;
});
});
await Promise.all(imagePromises);
previewModal.classList.remove('hidden');
hideLoader();
const downloadHandler = async () => {
showLoader('Generating High-Quality PDF...');
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
const { jsPDF } = window.jspdf;
const doc = new jsPDF({
orientation: 'p',
unit: 'pt',
format: 'letter'
});
await doc.html(previewContent, {
callback: function (doc: any) {
const links = previewContent.querySelectorAll('a');
const pageHeight = doc.internal.pageSize.getHeight();
const containerRect = previewContent.getBoundingClientRect(); // Get container's position
links.forEach(link => {
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;
const pageNum = Math.floor(relativeY / pageHeight) + 1;
const yOnPage = relativeY % pageHeight;
doc.setPage(pageNum);
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);
}
});
const outputFileName = `${file.name.replace(/\.[^/.]+$/, "")}.pdf`;
doc.save(outputFileName);
hideLoader();
},
autoPaging: 'slice',
x: 45,
y: 45,
width: 522,
windowWidth: previewContent.scrollWidth
});
};
const closeHandler = () => {
previewModal.classList.add('hidden');
previewContent.innerHTML = '';
downloadBtn.removeEventListener('click', downloadHandler);
closeBtn.removeEventListener('click', closeHandler);
};
downloadBtn.addEventListener('click', downloadHandler);
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}`);
}
}
+117
View File
@@ -0,0 +1,117 @@
import { categories } from './config/tools.js';
import { dom, switchView, hideAlert } from './ui.js';
import { setupToolInterface } from './handlers/toolSelectionHandler.js';
import { createIcons, icons } from 'lucide';
import * as pdfjsLib from 'pdfjs-dist';
import "../css/styles.css";
const init = () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
dom.toolGrid.textContent = '';
categories.forEach(category => {
const categoryGroup = document.createElement('div');
categoryGroup.className = 'category-group col-span-full';
const title = document.createElement('h2');
title.className = 'text-xl font-bold text-indigo-400 mb-4 mt-8 first:mt-0';
title.textContent = category.name;
const toolsContainer = document.createElement('div');
toolsContainer.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4 md:gap-6';
category.tools.forEach(tool => {
const toolCard = document.createElement('div');
toolCard.className = 'tool-card bg-gray-800 rounded-xl p-4 cursor-pointer flex flex-col items-center justify-center text-center';
toolCard.dataset.toolId = tool.id;
const icon = document.createElement('i');
icon.className = 'w-10 h-10 mb-3 text-indigo-400';
icon.setAttribute('data-lucide', tool.icon);
const toolName = document.createElement('h3');
toolName.className = 'font-semibold text-white';
toolName.textContent = tool.name;
toolCard.append(icon, toolName);
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);
}
toolsContainer.appendChild(toolCard);
});
categoryGroup.append(title, toolsContainer);
dom.toolGrid.appendChild(categoryGroup);
});
const searchBar = document.getElementById('search-bar');
const categoryGroups = dom.toolGrid.querySelectorAll('.category-group');
searchBar.addEventListener('input', () => {
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
const searchTerm = searchBar.value.toLowerCase().trim();
categoryGroups.forEach(group => {
const toolCards = group.querySelectorAll('.tool-card');
let visibleToolsInCategory = 0;
toolCards.forEach(card => {
const toolName = card.querySelector('h3').textContent.toLowerCase();
const toolSubtitle = card.querySelector('p')?.textContent.toLowerCase() || '';
const isMatch = toolName.includes(searchTerm) || toolSubtitle.includes(searchTerm);
card.classList.toggle('hidden', !isMatch);
if (isMatch) {
visibleToolsInCategory++;
}
});
group.classList.toggle('hidden', visibleToolsInCategory === 0);
});
});
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);
+17
View File
@@ -0,0 +1,17 @@
export const state = {
activeTool: null,
files: [],
pdfDoc: null,
pdfPages: [],
currentPdfUrl: null,
};
// Resets the state when switching views or completing an operation.
export function resetState() {
state.activeTool = null;
state.files = [];
state.pdfDoc = null;
state.pdfPages = [];
state.currentPdfUrl = null;
document.getElementById('tool-content').innerHTML = '';
}
+1797
View File
File diff suppressed because it is too large Load Diff
+114
View File
@@ -0,0 +1,114 @@
const STANDARD_SIZES = {
'A4': { width: 595.28, height: 841.89 },
'Letter': { width: 612, height: 792 },
'Legal': { width: 612, height: 1008 },
'Tabloid': { width: 792, height: 1224 },
'A3': { width: 841.89, height: 1190.55 },
'A5': { width: 419.53, height: 595.28 },
};
export function getStandardPageName(width: any, height: any) {
const tolerance = 1; // Allow for minor floating point variations
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
if ((Math.abs(width - size.width) < tolerance && Math.abs(height - size.height) < tolerance) ||
(Math.abs(width - size.height) < tolerance && Math.abs(height - size.width) < tolerance)) {
return name;
}
}
return 'Custom';
}
export function convertPoints(points: any, unit: any) {
let result = 0;
switch (unit) {
case 'in':
result = points / 72;
break;
case 'mm':
result = (points / 72) * 25.4;
break;
case 'px':
result = points * (96 / 72); // Assuming 96 DPI
break;
default: // 'pt'
result = points;
break;
}
return result.toFixed(2);
}
export const hexToRgb = (hex: any) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16) / 255,
g: parseInt(result[2], 16) / 255,
b: parseInt(result[3], 16) / 255,
} : { r: 0, g: 0, b: 0 }; // Default to black
};
export const formatBytes = (bytes: any, decimals = 1) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
export const downloadFile = (blob: any, filename: any) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
export const readFileAsArrayBuffer = (file: any) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
reader.readAsArrayBuffer(file);
});
};
export function parsePageRanges(rangeString: any, totalPages: any) {
if (!rangeString || rangeString.trim() === '') {
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);
}
}
// @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);
}
+214
View File
@@ -0,0 +1,214 @@
import { describe, it, expect } from 'vitest';
import {
getStandardPageName,
convertPoints,
hexToRgb,
formatBytes,
parsePageRanges,
} from '../js/utils/helpers';
describe('helpers', () => {
describe('getStandardPageName', () => {
it('should identify A4 portrait', () => {
expect(getStandardPageName(595.28, 841.89)).toBe('A4');
});
it('should identify A4 landscape', () => {
expect(getStandardPageName(841.89, 595.28)).toBe('A4');
});
it('should identify Letter size', () => {
expect(getStandardPageName(612, 792)).toBe('Letter');
});
it('should identify Legal size', () => {
expect(getStandardPageName(612, 1008)).toBe('Legal');
});
it('should identify A3 size', () => {
expect(getStandardPageName(841.89, 1190.55)).toBe('A3');
});
it('should handle floating point variations within tolerance', () => {
expect(getStandardPageName(595.5, 841.9)).toBe('A4');
});
it('should return Custom for non-standard sizes', () => {
expect(getStandardPageName(600, 800)).toBe('Custom');
});
});
describe('convertPoints', () => {
it('should convert points to inches', () => {
expect(convertPoints(72, 'in')).toBe('1.00');
expect(convertPoints(144, 'in')).toBe('2.00');
});
it('should convert points to millimeters', () => {
expect(convertPoints(72, 'mm')).toBe('25.40');
});
it('should convert points to pixels', () => {
expect(convertPoints(72, 'px')).toBe('96.00');
});
it('should return points as is for pt unit', () => {
expect(convertPoints(100, 'pt')).toBe('100.00');
});
it('should default to points for unknown unit', () => {
expect(convertPoints(50, 'unknown')).toBe('50.00');
});
it('should handle decimal values', () => {
expect(convertPoints(36, 'in')).toBe('0.50');
});
});
describe('hexToRgb', () => {
it('should convert hex to RGB (with #)', () => {
const result = hexToRgb('#ff0000');
expect(result).toEqual({ r: 1, g: 0, b: 0 });
});
it('should convert hex to RGB (without #)', () => {
const result = hexToRgb('00ff00');
expect(result).toEqual({ r: 0, g: 1, b: 0 });
});
it('should handle blue color', () => {
const result = hexToRgb('#0000ff');
expect(result).toEqual({ r: 0, g: 0, b: 1 });
});
it('should handle white color', () => {
const result = hexToRgb('#ffffff');
expect(result).toEqual({ r: 1, g: 1, b: 1 });
});
it('should handle black color', () => {
const result = hexToRgb('#000000');
expect(result).toEqual({ r: 0, g: 0, b: 0 });
});
it('should handle gray color', () => {
const result = hexToRgb('#808080');
expect(result.r).toBeCloseTo(0.502, 2);
expect(result.g).toBeCloseTo(0.502, 2);
expect(result.b).toBeCloseTo(0.502, 2);
});
it('should return black for invalid hex', () => {
const result = hexToRgb('invalid');
expect(result).toEqual({ r: 0, g: 0, b: 0 });
});
it('should be case insensitive', () => {
const result = hexToRgb('#FF0000');
expect(result).toEqual({ r: 1, g: 0, b: 0 });
});
});
describe('formatBytes', () => {
it('should format 0 bytes', () => {
expect(formatBytes(0)).toBe('0 Bytes');
});
it('should format bytes', () => {
expect(formatBytes(500)).toBe('500 Bytes');
expect(formatBytes(1023)).toBe('1023 Bytes');
});
it('should format kilobytes', () => {
expect(formatBytes(1024)).toBe('1 KB');
expect(formatBytes(2048)).toBe('2 KB');
});
it('should format megabytes', () => {
expect(formatBytes(1048576)).toBe('1 MB');
expect(formatBytes(5242880)).toBe('5 MB');
});
it('should format gigabytes', () => {
expect(formatBytes(1073741824)).toBe('1 GB');
});
it('should handle custom decimal places', () => {
expect(formatBytes(1536, 2)).toBe('1.5 KB');
expect(formatBytes(1536, 0)).toBe('2 KB');
});
it('should handle decimal values', () => {
expect(formatBytes(1536)).toBe('1.5 KB');
});
});
describe('parsePageRanges', () => {
const totalPages = 10;
it('should return all pages for empty string', () => {
const result = parsePageRanges('', totalPages);
expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
it('should return all pages for whitespace', () => {
const result = parsePageRanges(' ', totalPages);
expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
it('should parse single page', () => {
const result = parsePageRanges('5', totalPages);
expect(result).toEqual([4]); // 0-indexed
});
it('should parse multiple single pages', () => {
const result = parsePageRanges('1,3,5', totalPages);
expect(result).toEqual([0, 2, 4]);
});
it('should parse page ranges', () => {
const result = parsePageRanges('1-3', totalPages);
expect(result).toEqual([0, 1, 2]);
});
it('should parse mixed ranges and single pages', () => {
const result = parsePageRanges('1,3-5,7', totalPages);
expect(result).toEqual([0, 2, 3, 4, 6]);
});
it('should handle spaces in input', () => {
const result = parsePageRanges(' 1 , 3 - 5 , 7 ', totalPages);
expect(result).toEqual([0, 2, 3, 4, 6]);
});
it('should remove duplicates and sort', () => {
const result = parsePageRanges('5,3,5,1-3', totalPages);
expect(result).toEqual([0, 1, 2, 4]);
});
it('should skip invalid page numbers', () => {
const result = parsePageRanges('0,1,15,5', totalPages);
expect(result).toEqual([0, 4]); // Only valid pages
});
it('should skip invalid ranges', () => {
const result = parsePageRanges('1-15,3-5', totalPages);
expect(result).toEqual([2, 3, 4]); // Only 3-5 is valid
});
it('should skip ranges where start > end', () => {
const result = parsePageRanges('5-3,1-2', totalPages);
expect(result).toEqual([0, 1]); // Only 1-2 is valid
});
it('should handle all pages explicitly', () => {
const result = parsePageRanges('1-10', totalPages);
expect(result).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
it('should skip non-numeric values', () => {
const result = parsePageRanges('1,abc,5', totalPages);
expect(result).toEqual([0, 4]);
});
});
});
+86
View File
@@ -0,0 +1,86 @@
import { describe, it, expect } from 'vitest';
import { singlePdfLoadTools, simpleTools, multiFileTools } from '@/js/config/pdf-tools';
describe('Tool Configuration Arrays', () => {
// --- Tests for singlePdfLoadTools ---
describe('singlePdfLoadTools', () => {
it('should be an array of non-empty strings', () => {
expect(Array.isArray(singlePdfLoadTools)).toBe(true);
expect(singlePdfLoadTools.length).toBeGreaterThan(0);
for (const tool of singlePdfLoadTools) {
expect(typeof tool).toBe('string');
expect(tool.length).toBeGreaterThan(0);
}
});
it('should have the correct number of tools', () => {
// This acts as a snapshot test to catch unexpected additions/removals.
expect(singlePdfLoadTools).toHaveLength(38);
});
it('should not contain any duplicate tools', () => {
const uniqueTools = new Set(singlePdfLoadTools);
expect(uniqueTools.size).toBe(singlePdfLoadTools.length);
});
});
// --- Tests for simpleTools ---
describe('simpleTools', () => {
it('should be an array of non-empty strings', () => {
expect(Array.isArray(simpleTools)).toBe(true);
expect(simpleTools.length).toBeGreaterThan(0);
simpleTools.forEach(tool => {
expect(typeof tool).toBe('string');
expect(tool.length).toBeGreaterThan(0);
});
});
it('should have the correct number of tools', () => {
expect(simpleTools).toHaveLength(5);
});
it('should not contain any duplicate tools', () => {
const uniqueTools = new Set(simpleTools);
expect(uniqueTools.size).toBe(simpleTools.length);
});
});
// --- Tests for multiFileTools ---
describe('multiFileTools', () => {
it('should be an array of non-empty strings', () => {
expect(Array.isArray(multiFileTools)).toBe(true);
expect(multiFileTools.length).toBeGreaterThan(0);
multiFileTools.forEach(tool => {
expect(typeof tool).toBe('string');
expect(tool.length).toBeGreaterThan(0);
});
});
it('should have the correct number of tools', () => {
expect(multiFileTools).toHaveLength(10);
});
it('should not contain any duplicate tools', () => {
const uniqueTools = new Set(multiFileTools);
expect(uniqueTools.size).toBe(multiFileTools.length);
});
});
// --- Cross-Category Uniqueness Test ---
describe('Uniqueness Across All Tool Categories', () => {
it('should ensure no tool exists in more than one category', () => {
const allTools = [
...singlePdfLoadTools,
...simpleTools,
...multiFileTools,
];
const uniqueTools = new Set(allTools);
// If the size of the Set is different from the length of the combined array,
// it means there was at least one duplicate tool across the categories.
expect(uniqueTools.size).toBe(allTools.length);
});
});
});
+32
View File
@@ -0,0 +1,32 @@
import { afterEach, vi } from 'vitest';
afterEach(() => {
document.body.innerHTML = '';
document.head.innerHTML = '';
});
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
+60
View File
@@ -0,0 +1,60 @@
import { state, resetState } from '@/js/state';
import { describe, it, expect, beforeEach } from 'vitest';
describe('State Management', () => {
// Test the initial state on import
describe('Initial State', () => {
it('should have the correct initial values', () => {
expect(state.activeTool).toBeNull();
expect(state.files).toEqual([]); // Use toEqual for arrays/objects
expect(state.pdfDoc).toBeNull();
expect(state.pdfPages).toEqual([]);
expect(state.currentPdfUrl).toBeNull();
});
});
// Test the resetState function
describe('resetState function', () => {
// Before each test in this block, we'll "dirty" the state
// to ensure the reset function is actually doing something.
beforeEach(() => {
// 1. Modify the state properties to non-default values
state.activeTool = 'merge';
state.files = [{ name: 'dummy.pdf', size: 1234 } as File];
state.pdfDoc = { numPages: 5 }; // Mock PDF document object
state.pdfPages = [{}, {}]; // Mock page objects
state.currentPdfUrl = 'blob:http://localhost/some-uuid';
// 2. Create the DOM element that the function interacts with
// The setup.ts file will clean this up automatically after each test.
document.body.innerHTML = '<div id="tool-content">Some old tool content</div>';
});
it('should reset all state properties to their initial values', () => {
// Call the function to test
resetState();
// Assert that all properties are back to their default state
expect(state.activeTool).toBeNull();
expect(state.files).toEqual([]);
expect(state.pdfDoc).toBeNull();
expect(state.pdfPages).toEqual([]);
expect(state.currentPdfUrl).toBeNull();
});
it('should clear the innerHTML of the #tool-content element', () => {
const toolContentElement = document.getElementById('tool-content');
// Sanity check: ensure the content exists before resetting
expect(toolContentElement?.innerHTML).toBe('Some old tool content');
// Call the function to test
resetState();
// Assert that the element's content has been cleared
expect(toolContentElement?.innerHTML).toBe('');
});
});
});
+52
View File
@@ -0,0 +1,52 @@
// toolCategories.test.ts
import { categories } from '@/js/config/tools';
import { describe, it, expect } from 'vitest';
describe('Tool Categories Configuration', () => {
// 1. Basic Structure and Type Checking
it('should be an array of category objects', () => {
expect(Array.isArray(categories)).toBe(true);
expect(categories.length).toBeGreaterThan(0);
});
// 2. Loop through each category to perform specific checks
describe.each(categories)('Category: "$name"', (category) => {
// Check that the category object itself is well-formed
it('should have a non-empty "name" string and a non-empty "tools" array', () => {
expect(typeof category.name).toBe('string');
expect(category.name.length).toBeGreaterThan(0);
expect(Array.isArray(category.tools)).toBe(true);
expect(category.tools.length).toBeGreaterThan(0);
});
// **KEY CHANGE**: This test now ensures IDs are unique only WITHIN this specific category.
it('should not contain any duplicate tool IDs within its own list', () => {
const toolIds = category.tools.map(tool => tool.id);
const uniqueToolIds = new Set(toolIds);
// This assertion checks for duplicates inside THIS category only.
expect(uniqueToolIds.size).toBe(toolIds.length);
});
// 3. Loop through each tool inside the category to validate its schema
describe.each(category.tools)('Tool: "$name"', (tool) => {
it('should have the correct properties with non-empty string values', () => {
// Check for property existence
expect(tool).toHaveProperty('id');
expect(tool).toHaveProperty('name');
expect(tool).toHaveProperty('icon');
expect(tool).toHaveProperty('subtitle');
// Check for non-empty string types for each property
for (const key of ['id', 'name', 'icon', 'subtitle']) {
const value = tool[key as keyof typeof tool];
expect(typeof value).toBe('string');
expect(value.length).toBeGreaterThan(0);
}
});
});
});
});
+133
View File
@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terms and Conditions - Bentopdf</title>
<script src="https://unpkg.com/lucide@latest"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="icon" type="image/png" href="images/favicon.svg">
<link href="/src/css/styles.css" rel="stylesheet">
</head>
<body class="antialiased bg-gray-900 text-gray-300">
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16">
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
<img src="images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8 mr-2">
<span class="text-white font-bold text-xl">BentoPDF</span>
</a>
<div class="hidden md:flex items-center space-x-8">
<a href="index.html" class="nav-link">Home</a>
<a href="./about.html" class="nav-link">About</a>
<a href="./contact.html" class="nav-link">Contact</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
<div class="md:hidden flex items-center gap-4">
<a href="index.html" class="nav-link">Home</a>
<a href="index.html#tools-header" class="nav-link">All Tools</a>
</div>
</div>
</div>
</nav>
<div id="app" class="container mx-auto p-4 md:p-8">
<section class="max-w-4xl mx-auto py-12">
<h1 class="text-4xl md:text-5xl font-bold text-center text-white mb-4">Terms and Conditions</h1>
<p class="text-center text-gray-500">Last Updated: September 14, 2025</p>
<div class="legal-content mt-12">
<h2>1. Acceptance of Terms</h2>
<p>By accessing and using Bentopdf (the "Service"), you accept and agree to be bound by the terms and provision of this agreement. If you do not agree to abide by these terms, please do not use this service.</p>
<h2>2. Description of Service</h2>
<p>Bentopdf provides a suite of client-side tools for processing and manipulating Portable Document Format (PDF) files. All operations performed by the Service are executed locally within your web browser. <strong>No files or data are uploaded to or stored on our servers.</strong></p>
<h2>3. User Conduct and Responsibilities</h2>
<p>You are solely responsible for the content of the files you process with our Service. You agree not to use the Service for any unlawful purpose, including but not limited to:</p>
<ul>
<li>Processing any material that infringes on the copyright, trademark, or intellectual property rights of others.</li>
<li>Processing any material that is defamatory, libelous, obscene, or otherwise illegal.</li>
<li>Attempting to reverse-engineer, decompile, or otherwise disrupt the functionality of the Service.</li>
</ul>
<h2>4. Disclaimer of Warranties</h2>
<p>The Service is provided "as is" and "as available" without any warranties of any kind, express or implied. We do not warrant that the service will be error-free, uninterrupted, or that the results obtained from using the tools will be accurate, complete, or reliable. You acknowledge that you use the Service at your own risk.</p>
<p>Specifically, we do not guarantee that file conversions, compressions, or modifications will be perfect. Data loss or corruption, while unlikely, is a possibility. It is your responsibility to maintain backups of your original files.</p>
<h2>5. Limitation of Liability</h2>
<p>To the fullest extent permitted by applicable law, in no event shall Bentopdf, its developers, or its affiliates be liable for any indirect, incidental, special, consequential, or punitive damages, including but not limited to, loss of profits, data, use, goodwill, or other intangible losses, resulting from:</p>
<ul>
<li>Your access to or use of or inability to access or use the Service.</li>
<li>Any conduct or content of any third party on the Service.</li>
<li>Any content obtained from the Service.</li>
<li>Unauthorized access, use, or alteration of your transmissions or content.</li>
</ul>
<p>Our total liability to you for any and all claims arising out of your use of this free service shall not exceed the amount of zero dollars ($0.00).</p>
<h2>6. Intellectual Property</h2>
<p>The visual interfaces, graphics, design, compilation, information, computer code, products, software, services, and all other elements of the Service provided by Bentopdf are protected by intellectual property and other laws. All materials contained on the Service are the property of Bentopdf or our third-party licensors.</p>
<h2>7. Governing Law</h2>
<p>These Terms shall be governed and construed in accordance with the laws of India, without regard to its conflict of law provisions.</p>
<h2>8. Changes to Terms</h2>
<p>We reserve the right, at our sole discretion, to modify or replace these Terms at any time. We will provide notice of changes by updating the "Last Updated" date at the top of this page. By continuing to access or use our Service after those revisions become effective, you agree to be bound by the revised terms.</p>
<h2>9. Contact Us</h2>
<p>If you have any questions about these Terms, please contact us at <a href="mailto:[email protected]">[email protected]</a>.</p>
</div>
</section>
</div>
<footer class="mt-16 border-t-2 border-gray-700 py-8">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
<div class="mb-8 md:mb-0">
<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">
<span class="text-xl font-bold text-white">BentoPDF</span>
</div>
<p class="text-gray-400 text-sm">&copy; 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>
</footer>
<script>
lucide.createIcons();
</script>
</body>
</html>
+66
View File
@@ -0,0 +1,66 @@
// {
// "compilerOptions": {
// "target": "ES2022",
// "useDefineForClassFields": true,
// "module": "ESNext",
// "lib": ["ES2022", "DOM", "DOM.Iterable"],
// "types": ["vite/client"],
// "skipLibCheck": true,
// /* Bundler mode */
// "moduleResolution": "bundler",
// "allowImportingTsExtensions": true,
// "verbatimModuleSyntax": true,
// "moduleDetection": "force",
// "noEmit": true,
// /* Linting */
// "strict": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "erasableSyntaxOnly": true,
// "noFallthroughCasesInSwitch": true,
// "noUncheckedSideEffectImports": true
// },
// "include": ["src"]
// }
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client", "vitest/globals"], // Added vitest/globals
/* Easier module handling for Vite */
"moduleResolution": "bundler",
"allowJs": true,
"checkJs": false,
"skipLibCheck": true,
"noEmit": true,
"allowImportingTsExtensions": true,
/* Disable strict checks for now */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": false,
"noUncheckedSideEffectImports": false,
/* Fix for ArrayBuffer type issues */
"noImplicitAny": false,
/* Quality-of-life options */
"isolatedModules": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
/* Path aliases (optional but helpful) */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src", "src/**/*.ts"], // Updated to include all TS files
"exclude": ["node_modules", "dist"]
}
+55
View File
@@ -0,0 +1,55 @@
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite'
import { nodePolyfills } from 'vite-plugin-node-polyfills';
import { resolve } from 'path';
export default defineConfig({
plugins: [
tailwindcss(),
nodePolyfills({
include: ['buffer', 'stream', 'util', 'zlib', 'process'],
globals: {
Buffer: true,
global: true,
process: true,
},
}),
],
resolve: {
alias: {
stream: 'stream-browserify',
zlib: 'browserify-zlib',
},
},
optimizeDeps: {
include: ['pdfkit', 'blob-stream'],
},
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
about: resolve(__dirname, 'about.html'),
contact: resolve(__dirname, 'contact.html'),
faq: resolve(__dirname, 'faq.html'),
privacy: resolve(__dirname, 'privacy.html'),
terms: resolve(__dirname, 'terms.html'),
},
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/tests/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/tests/',
'*.config.ts',
'**/*.d.ts',
'dist/',
],
},
},
});
+55
View File
@@ -0,0 +1,55 @@
import { defineConfig } from 'vitest/config';
import { resolve } from 'path';
export default defineConfig({
test: {
// Enable global test APIs (describe, it, expect, etc.)
globals: true,
// Simulate browser environment
environment: 'jsdom',
// Setup files to run before tests
setupFiles: ['./src/tests/setup.ts'],
// Coverage configuration
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
reportsDirectory: './coverage',
exclude: [
'node_modules/',
'src/tests/',
'dist/',
'*.config.ts',
'*.config.js',
'**/*.d.ts',
'public/',
'scripts/',
],
// Set thresholds (optional)
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
// Include/exclude patterns
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'],
// Test timeout
testTimeout: 10000,
// Watch options
watch: false,
},
resolve: {
alias: {
'@': resolve(__dirname, './src'),
},
},
});