mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-09 17:14:16 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb766ba5a1 | ||
|
|
0cad5e180e | ||
|
|
0999163d3c | ||
|
|
5b7c09ddf3 | ||
|
|
7a4a257438 | ||
|
|
e4bd206d17 | ||
|
|
05b5181b3e | ||
|
|
bc181c8c96 | ||
|
|
661c030ae1 | ||
|
|
9befab7758 | ||
|
|
925eb5db28 | ||
|
|
2de6cdc7c6 | ||
|
|
0ab113f8ba | ||
|
|
e7e0160fe9 | ||
|
|
f0c6b1e562 | ||
|
|
9bd193cd9d | ||
|
|
b8cd43c9ec | ||
|
|
400ec9aadf | ||
|
|
f40108627e | ||
|
|
e64215a022 | ||
|
|
1707b293a8 | ||
|
|
af75a3d74f | ||
|
|
ad49adb744 | ||
|
|
392b810c54 | ||
|
|
090146c600 | ||
|
|
72e74d2a62 | ||
|
|
c49957b721 | ||
|
|
72688cde4c | ||
|
|
da6610fa65 | ||
|
|
e89dd5e116 | ||
|
|
005ff78f6a | ||
|
|
d3fb7e6330 | ||
|
|
b3bccb48f1 | ||
|
|
e0a253be83 | ||
|
|
24b5204098 | ||
|
|
89f422b2af | ||
|
|
12753349ec | ||
|
|
f3e7ef25fa | ||
|
|
ba20536436 | ||
|
|
874d97889f | ||
|
|
e3468e3aaa | ||
|
|
922e42548f | ||
|
|
e0d3075609 | ||
|
|
466646d15b | ||
|
|
8ac62b48a6 | ||
|
|
be6c15fef2 | ||
|
|
d5a15bce78 | ||
|
|
3cbbc42434 | ||
|
|
06c36a590b | ||
|
|
de3c2fecc2 | ||
|
|
b7de4bf1ed | ||
|
|
e0e5d50240 | ||
|
|
11ba3fe81b | ||
|
|
05de28d791 | ||
|
|
0403a9665f | ||
|
|
0ddf54cba3 | ||
|
|
25c221f8b9 | ||
|
|
d4f1aa30f5 | ||
|
|
c9faee4fd9 | ||
|
|
3fc873d343 | ||
|
|
739dac55ee | ||
|
|
dbc1b8eb7f | ||
|
|
11c24c1ff1 | ||
|
|
929aa7e132 | ||
|
|
2b85a759af | ||
|
|
9af9c33f74 | ||
|
|
835c3c9423 | ||
|
|
a8df98f20d | ||
|
|
11c9e14803 | ||
|
|
4e2d04fb9c | ||
|
|
edc0f27e0f | ||
|
|
bf57f36cb5 | ||
|
|
6ad6a7d730 | ||
|
|
0e7c53560a | ||
|
|
59b351eee4 | ||
|
|
48fe49ac37 | ||
|
|
ccb1b119c5 | ||
|
|
6971c18e5e | ||
|
|
8a72063ef7 | ||
|
|
4e6cc77a28 | ||
|
|
f037e27535 | ||
|
|
6270080045 | ||
|
|
f9a06b54f5 | ||
|
|
584f2f09d1 | ||
|
|
8ac4793f9b | ||
|
|
d163bd29e6 | ||
|
|
34a00ad74a | ||
|
|
797d8f5b9d | ||
|
|
ef7b9d0365 | ||
|
|
6bfcb30592 | ||
|
|
a605e56834 | ||
|
|
2e2c634b41 | ||
|
|
caf23b0ab9 | ||
|
|
88e05e6713 | ||
|
|
b3ab445538 | ||
|
|
ddeebc8142 | ||
|
|
8a494c0a01 | ||
|
|
f11efe7278 | ||
|
|
8e549b7304 | ||
|
|
574ed4bd36 | ||
|
|
7d1a1c9064 | ||
|
|
560d45d290 | ||
|
|
a48ce08d39 | ||
|
|
f8579fb291 | ||
|
|
f9ef634b6f | ||
|
|
6841b38eac | ||
|
|
d98e18b51f | ||
|
|
cc83d7951f | ||
|
|
298135ab0d | ||
|
|
a61ec759ec | ||
|
|
d33a02b1a5 | ||
|
|
f1d830d81d | ||
|
|
87c191213c | ||
|
|
2e53b68631 | ||
|
|
17557d294b | ||
|
|
886b81eb4f | ||
|
|
70030fedda | ||
|
|
48baad9bf9 | ||
|
|
a82148c253 | ||
|
|
39080c800f | ||
|
|
d4633e1429 | ||
|
|
1cf40d9328 |
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
github: alam00000
|
||||||
|
ko_fi: alio01
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
name: '🐛 Bug / 💡 Feature / ❓ Question'
|
||||||
|
about: 'Report a bug, request a feature, or ask a question about BentoPDF'
|
||||||
|
title: '(Bug) <short title>, (Feature) <short title>, or (Question) <short title>'
|
||||||
|
labels: ['needs triage']
|
||||||
|
assignees: []
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type of Issue
|
||||||
|
|
||||||
|
Please check one:
|
||||||
|
|
||||||
|
- [ ] 🐛 Bug Report <!-- Label: bug -->
|
||||||
|
- [ ] 💡 Feature Request <!-- Label: feature -->
|
||||||
|
- [ ] ❓ Question / Help <!-- Label: question -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Provide a clear and concise description of the issue, feature request, or question.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Steps to Reproduce (for Bugs)
|
||||||
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Run '...'
|
||||||
|
3. Observe error: '...'
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
Describe what you expected BentoPDF to do.
|
||||||
|
|
||||||
|
**Actual Behavior:**
|
||||||
|
Describe what actually happened, including error messages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Request Details (if applicable)
|
||||||
|
|
||||||
|
- What functionality are you requesting?
|
||||||
|
- Why is this useful?
|
||||||
|
- Any example or context to illustrate it?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Question Details (if applicable)
|
||||||
|
|
||||||
|
- What is your question?
|
||||||
|
- What have you tried so far?
|
||||||
|
- Any relevant code snippet or scenario?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Screenshots / Logs (if applicable)
|
||||||
|
|
||||||
|
Attach any screenshots, logs, or stack traces that help explain the problem or question.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- **OS:** (e.g., macOS 14.0 / Ubuntu 22.04 / Windows 11)
|
||||||
|
- **Dependencies / setup details (if any):**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💭 Additional Context
|
||||||
|
|
||||||
|
Any other information, suggestions, or references that might help maintainers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
✅ **Title Format Reminder:**
|
||||||
|
|
||||||
|
- `(Bug) Text alignment incorrect on multi-line paragraphs`
|
||||||
|
- `(Feature) Add support for custom PDF metadata`
|
||||||
|
- `(Question) How to embed custom fonts?`
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: 'npm'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
|
groups:
|
||||||
|
npm-dependencies:
|
||||||
|
patterns:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
- package-ecosystem: 'github-actions'
|
||||||
|
directory: '/'
|
||||||
|
schedule:
|
||||||
|
interval: 'daily'
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
### Description
|
||||||
|
|
||||||
|
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||||
|
|
||||||
|
Fixes # (issue)
|
||||||
|
|
||||||
|
### Type of change
|
||||||
|
|
||||||
|
Please delete options that are not relevant.
|
||||||
|
|
||||||
|
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||||
|
- [ ] New feature (non-breaking change which adds functionality)
|
||||||
|
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
|
- [ ] This change requires a documentation update
|
||||||
|
|
||||||
|
### 🧪 How Has This Been Tested?
|
||||||
|
|
||||||
|
Please describe **how you tested your changes** so that maintainers can verify them. Include enough detail for someone else to reproduce your tests.
|
||||||
|
|
||||||
|
**Checklist:**
|
||||||
|
|
||||||
|
- [ ] Verified output manually
|
||||||
|
- [ ] Tested with relevant sample documents or data
|
||||||
|
- [ ] Wrote Vite Test Case (if applicable)
|
||||||
|
|
||||||
|
**Expected Results:**
|
||||||
|
|
||||||
|
- What the tests should produce (e.g., correct PDF rendering, no errors).
|
||||||
|
|
||||||
|
**Actual Results:**
|
||||||
|
|
||||||
|
- What you observed when running the tests.
|
||||||
|
|
||||||
|
### Checklist:
|
||||||
|
|
||||||
|
- [ ] My code follows the style guidelines of this project
|
||||||
|
- [ ] I have performed a self-review of my own code
|
||||||
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
|
- [ ] I have made corresponding changes to the documentation
|
||||||
|
- [ ] My changes generate no new warnings
|
||||||
|
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||||
|
- [ ] New and existing unit tests pass locally with my changes
|
||||||
|
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||||
@@ -1,30 +1,69 @@
|
|||||||
name: build docker image and push to dockerhub
|
name: Build and Push Docker Images (Default + Simple Mode)
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- 'main'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-build-and-push:
|
docker-build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
mode:
|
||||||
|
- name: default
|
||||||
|
simple_mode: false
|
||||||
|
suffix: ''
|
||||||
|
- name: simple
|
||||||
|
simple_mode: true
|
||||||
|
suffix: '-simple'
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: Set up QEMU
|
||||||
name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
-
|
- name: Set up Docker Buildx
|
||||||
name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
-
|
- name: Login to DockerHub
|
||||||
name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
-
|
- name: Login to GitHub Container Registry
|
||||||
name: Build and push
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version_without_v=${VERSION#v}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "version=latest" >> $GITHUB_OUTPUT
|
||||||
|
echo "version_without_v=latest" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build and push ${{ matrix.mode.name }} image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
tags: bentopdf/bentopdf:latest
|
build-args: |
|
||||||
|
SIMPLE_MODE=${{ matrix.mode.simple_mode }}
|
||||||
|
tags: |
|
||||||
|
bentopdf/bentopdf${{ matrix.mode.suffix }}:latest
|
||||||
|
bentopdf/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version_without_v }}
|
||||||
|
bentopdf/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}
|
||||||
|
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:latest
|
||||||
|
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version_without_v }}
|
||||||
|
ghcr.io/${{ github.repository_owner }}/bentopdf${{ matrix.mode.suffix }}:${{ steps.version.outputs.version }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
+35
-1
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# Contributing to BentoPDF
|
# Contributing to BentoPDF
|
||||||
|
|
||||||
First off, thank you for considering contributing to **BentoPDF**! Your help makes this project better for everyone.
|
First off, thank you for considering contributing to **BentoPDF**! Your help makes this project better for everyone.
|
||||||
@@ -19,9 +18,39 @@ You can contribute in several ways:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Issue Template
|
||||||
|
|
||||||
|
When reporting bugs, requesting features, or asking questions, please use our [issue template](.github/ISSUE_TEMPLATE/bug_feature_question.md). The template will automatically appear when you create a new issue.
|
||||||
|
|
||||||
|
**Our issue template helps you provide:**
|
||||||
|
|
||||||
|
- Clear categorization (Bug, Feature Request, or Question)
|
||||||
|
- Steps to reproduce (for bugs)
|
||||||
|
- Expected vs actual behavior
|
||||||
|
- Environment details
|
||||||
|
- Screenshots or logs
|
||||||
|
|
||||||
|
**Title Format Examples:**
|
||||||
|
|
||||||
|
- `(Bug) Text alignment incorrect on multi-line paragraphs`
|
||||||
|
- `(Feature) Add support for custom PDF metadata`
|
||||||
|
- `(Question) How to embed custom fonts?`
|
||||||
|
|
||||||
|
### Pull Request Template
|
||||||
|
|
||||||
|
When submitting code contributions, please use our [pull request template](.github/pull_request_template.md). The template will automatically appear when you create a new PR.
|
||||||
|
|
||||||
|
**Key requirements:**
|
||||||
|
|
||||||
|
- Link to the related issue (e.g., `Fixes #123`)
|
||||||
|
- Describe the type of change (bug fix, feature, breaking change)
|
||||||
|
- Explain how you tested your changes
|
||||||
|
- Complete the checklist before submitting
|
||||||
|
|
||||||
## 2. Getting Started with Code Contributions
|
## 2. Getting Started with Code Contributions
|
||||||
|
|
||||||
1. **Fork the Repository**
|
1. **Fork the Repository**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/alam00000/bentopdf.git
|
git clone https://github.com/alam00000/bentopdf.git
|
||||||
cd bento-pdf
|
cd bento-pdf
|
||||||
@@ -29,6 +58,7 @@ You can contribute in several ways:
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Create a New Branch**
|
2. **Create a New Branch**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout -b feature/my-new-feature
|
git checkout -b feature/my-new-feature
|
||||||
```
|
```
|
||||||
@@ -39,20 +69,24 @@ You can contribute in several ways:
|
|||||||
- Update or add tests if applicable.
|
- Update or add tests if applicable.
|
||||||
|
|
||||||
4. **Run Tests**
|
4. **Run Tests**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run test
|
npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Commit Your Changes**
|
5. **Commit Your Changes**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add .
|
git add .
|
||||||
git commit -m "Add a meaningful commit message"
|
git commit -m "Add a meaningful commit message"
|
||||||
```
|
```
|
||||||
|
|
||||||
6. **Push and Submit a Pull Request**
|
6. **Push and Submit a Pull Request**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git push origin feature/my-new-feature
|
git push origin feature/my-new-feature
|
||||||
```
|
```
|
||||||
|
|
||||||
- Open a pull request on GitHub and provide a clear description of your changes.
|
- Open a pull request on GitHub and provide a clear description of your changes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+37
-7
@@ -1,22 +1,52 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build without type checking (vite build only)
|
# Build without type checking (vite build only)
|
||||||
|
# Pass SIMPLE_MODE environment variable if provided
|
||||||
|
ARG SIMPLE_MODE=false
|
||||||
|
ENV SIMPLE_MODE=$SIMPLE_MODE
|
||||||
RUN npm run build -- --mode production
|
RUN npm run build -- --mode production
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM nginx:alpine
|
FROM nginxinc/nginx-unprivileged:stable-alpine-slim
|
||||||
|
|
||||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
LABEL org.opencontainers.image.source="https://github.com/alam00000/bentopdf"
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
EXPOSE 80
|
COPY --chown=nginx:nginx --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY --chown=nginx:nginx nginx.conf /etc/nginx/nginx.conf
|
||||||
|
RUN mkdir -p /etc/nginx/tmp && chown -R nginx:nginx /etc/nginx/tmp
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
||||||
|
|
||||||
|
# Old Dockerfile for Root User
|
||||||
|
# # Build stage
|
||||||
|
# FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
# WORKDIR /app
|
||||||
|
|
||||||
|
# COPY package*.json ./
|
||||||
|
# RUN npm ci
|
||||||
|
|
||||||
|
# COPY . .
|
||||||
|
|
||||||
|
# # Build without type checking (vite build only)
|
||||||
|
# # Pass SIMPLE_MODE environment variable if provided
|
||||||
|
# ARG SIMPLE_MODE=false
|
||||||
|
# ENV SIMPLE_MODE=$SIMPLE_MODE
|
||||||
|
# RUN npm run build -- --mode production
|
||||||
|
|
||||||
|
# # Production stage
|
||||||
|
# FROM nginx:alpine
|
||||||
|
|
||||||
|
# COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
# COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# EXPOSE 8080
|
||||||
|
|
||||||
|
# CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -1,161 +1,661 @@
|
|||||||
Apache License
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 2.0, January 2004
|
Version 3, 19 November 2007
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
1. Definitions.
|
Preamble
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
The licenses for most software and other practical works are designed
|
||||||
the copyright owner that is granting the License.
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
When we speak of free software, we are referring to freedom, not
|
||||||
other entities that control, are controlled by, or are under common
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
control with that entity. For the purposes of this definition,
|
have the freedom to distribute copies of free software (and charge for
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
them if you wish), that you receive source code or can get it if you
|
||||||
direction or management of such entity, whether by contract or
|
want it, that you can change the software or use pieces of it in new
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
free programs, and that you know you can do these things.
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
Developers that use our General Public Licenses protect your rights
|
||||||
exercising permissions granted by this License.
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
A secondary benefit of defending all users' freedom is that
|
||||||
including but not limited to software source code, documentation
|
improvements made in alternate versions of the program, if they
|
||||||
source, and configuration files.
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
The GNU Affero General Public License is designed specifically to
|
||||||
transformation or translation of a Source form, including but not
|
ensure that, in such cases, the modified source code becomes available
|
||||||
limited to compiled object code, generated documentation, and
|
to the community. It requires the operator of a network server to
|
||||||
conversions to other media types.
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
An older license, called the Affero General Public License and
|
||||||
Object form, made available under the License, as indicated by a
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
copyright notice that is included in or attached to the work.
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
The precise terms and conditions for copying, distribution and
|
||||||
form, that is based on (or derived from) the Work and for which the
|
modification follow.
|
||||||
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
|
TERMS AND CONDITIONS
|
||||||
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
|
0. Definitions.
|
||||||
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" refers to version 3 of the GNU Affero General Public License.
|
||||||
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
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
works, such as semiconductor masks.
|
||||||
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
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
modifications, and in Source or Object form, provided that You
|
"recipients" may be individuals or organizations.
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
Derivative Works a copy of this License; and
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
A "covered work" means either the unmodified Program or a work based
|
||||||
stating that You changed the files; and
|
on the Program.
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
To "propagate" a work means to do anything with it that, without
|
||||||
that You distribute, all copyright, patent, trademark, and
|
permission, would make you directly or secondarily liable for
|
||||||
attribution notices from the Source form of the Work,
|
infringement under applicable copyright law, except executing it on a
|
||||||
excluding those notices that do not pertain to any part of
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
the Derivative Works; and
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
To "convey" a work means any kind of propagation that enables other
|
||||||
distribution, then any Derivative Works that You distribute must
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
include a readable copy of the attribution notices contained
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
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
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
may provide additional or different license terms and conditions
|
to the extent that it includes a convenient and prominently visible
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
tells the user that there is no warranty for the work (except to the
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
extent that warranties are provided), that licensees may convey the
|
||||||
the conditions stated in this License.
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
1. Source Code.
|
||||||
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
|
The "source code" for a work means the preferred form of the work
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
for making modifications to it. "Object code" means any non-source
|
||||||
except as required for reasonable and customary use in describing the
|
form of a work.
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
A "Standard Interface" means an interface that either is an official
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
standard defined by a recognized standards body, or, in the case of
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
interfaces specified for a particular programming language, one that
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
is widely used among developers working in that language.
|
||||||
implied.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
The "System Libraries" of an executable work include anything, other
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
shall any Contributor be liable to You for damages arising as a
|
packaging a Major Component, but which is not part of that Major
|
||||||
result of this License or out of the use or inability to use the
|
Component, and (b) serves only to enable use of the work with that
|
||||||
Work.
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
The "Corresponding Source" for a work in object code form means all
|
||||||
the Work or Derivative Works thereof, You may choose to offer
|
the source code needed to generate, install, and (for an executable
|
||||||
acceptance of support, warranty, indemnity, or other liability
|
work) run the object code and to modify the work, including scripts to
|
||||||
obligations, but only on Your own behalf.
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
If you develop a new program, and you want it to be of the greatest
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
possible use to the public, the best way to achieve this is to make it
|
||||||
replaced with your own identifying information. (Don't include
|
free software which everyone can redistribute and change under these terms.
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format.
|
|
||||||
|
|
||||||
Copyright 2025 Abdullah Alam
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
you may not use this file except in compliance with the License.
|
Copyright (C) <year> <name of author>
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
This program is distributed in the hope that it will be useful,
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
See the License for the specific language governing permissions and
|
GNU Affero General Public License for more details.
|
||||||
limitations under the License.
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
# BentoPDF
|
# BentoPDF
|
||||||
|
|
||||||

|
|
||||||
[](https://ko-fi.com/alio0)
|
|
||||||
|
|
||||||
**BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that allows you to manipulate, edit, merge, and process PDF files directly in your browser. No server-side processing is required, ensuring your files remain secure and private.
|
**BentoPDF** is a powerful, privacy-first, client-side PDF toolkit that allows you to manipulate, edit, merge, and process PDF files directly in your browser. No server-side processing is required, ensuring your files remain secure and private.
|
||||||
|
|
||||||
|
 [](https://ko-fi.com/alio0) 
|
||||||
|
[](https://github.com/sponsors/alam00000)
|
||||||
|
|
||||||
|
## ⭐ Stargazers over time
|
||||||
|
|
||||||
|
[](https://star-history.com/#alam00000/bentopdf&Date)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✨ Why BentoPDF?
|
## ✨ Why BentoPDF?
|
||||||
@@ -21,8 +25,9 @@
|
|||||||
BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
||||||
|
|
||||||
### Organize & Manage PDFs
|
### Organize & Manage PDFs
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :--- | :--- |
|
| :------------------------ | :------------------------------------------------------------------------- |
|
||||||
| **Merge PDFs** | Combine multiple PDF files into one. |
|
| **Merge PDFs** | Combine multiple PDF files into one. |
|
||||||
| **Split PDFs** | Extract specific pages or divide a document into smaller files. |
|
| **Split PDFs** | Extract specific pages or divide a document into smaller files. |
|
||||||
| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. |
|
| **Organize Pages** | Reorder, duplicate, or delete pages with a simple drag-and-drop interface. |
|
||||||
@@ -31,10 +36,13 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
|||||||
| **Rotate PDF** | Rotate individual or all pages in a document. |
|
| **Rotate PDF** | Rotate individual or all pages in a document. |
|
||||||
| **N-Up PDF** | Combine multiple pages onto a single page. |
|
| **N-Up PDF** | Combine multiple pages onto a single page. |
|
||||||
| **View PDF** | A powerful, integrated PDF viewer. |
|
| **View PDF** | A powerful, integrated PDF viewer. |
|
||||||
|
| **Alternate & Mix pages** | Merge pages by alternating pages from each PDF. |
|
||||||
|
| **Posterize PDF** | Split a PDF into multiple smaller pages for print. |
|
||||||
|
|
||||||
### Edit & Modify PDFs
|
### Edit & Modify PDFs
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :--- | :--- |
|
| :--------------------- | :---------------------------------------------------------- |
|
||||||
| **PDF Editor** | A comprehensive editor to modify your PDFs. |
|
| **PDF Editor** | A comprehensive editor to modify your PDFs. |
|
||||||
| **Add Page Numbers** | Easily add page numbers with customizable formatting. |
|
| **Add Page Numbers** | Easily add page numbers with customizable formatting. |
|
||||||
| **Add Watermark** | Add text or image watermarks to protect your documents. |
|
| **Add Watermark** | Add text or image watermarks to protect your documents. |
|
||||||
@@ -46,24 +54,31 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
|||||||
| **Fill Forms** | Fill out PDF forms directly in your browser. |
|
| **Fill Forms** | Fill out PDF forms directly in your browser. |
|
||||||
| **Flatten PDF** | Flatten form fields and annotations into static content. |
|
| **Flatten PDF** | Flatten form fields and annotations into static content. |
|
||||||
| **Remove Annotations** | Remove comments, highlights, and other annotations. |
|
| **Remove Annotations** | Remove comments, highlights, and other annotations. |
|
||||||
|
| **Remove Blank Pages** | Auto detect and remove blank pages in a PDF. |
|
||||||
|
| **Edit Bookmarks** | Add, Edit, Create, Import and Export PDF Bookmarks. |
|
||||||
|
|
||||||
### Convert to PDF
|
### Convert to PDF
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :--- | :--- |
|
| :------------------ | :-------------------------------------------------------------- |
|
||||||
| **Image to PDF** | Convert JPG, PNG, WebP, SVG, BMP, HEIC, and TIFF images to PDF. |
|
| **Image to PDF** | Convert JPG, PNG, WebP, SVG, BMP, HEIC, and TIFF images to PDF. |
|
||||||
| **Markdown to PDF** | Convert `.md` files into professional PDF documents. |
|
| **Markdown to PDF** | Convert `.md` files into professional PDF documents. |
|
||||||
| **Text to PDF** | Convert plain text files into a PDF. |
|
| **Text to PDF** | Convert plain text files into a PDF. |
|
||||||
|
| **JSON to PDF** | Convert JSON to PDF. |
|
||||||
|
|
||||||
### Convert from PDF
|
### Convert from PDF
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :--- | :--- |
|
| :------------------- | :----------------------------------------------------------------------------- |
|
||||||
| **PDF to Image** | Convert PDF pages to JPG, PNG, WebP, BMP, or TIFF formats. |
|
| **PDF to Image** | Convert PDF pages to JPG, PNG, WebP, BMP, or TIFF formats. |
|
||||||
| **PDF to Greyscale** | Convert a color PDF into a black-and-white version. |
|
| **PDF to Greyscale** | Convert a color PDF into a black-and-white version. |
|
||||||
| **OCR PDF** | Make scanned PDFs searchable and copyable using Optical Character Recognition. |
|
| **OCR PDF** | Make scanned PDFs searchable and copyable using Optical Character Recognition. |
|
||||||
|
| **PDF to JSON** | Convert JSON to PDFs. |
|
||||||
|
|
||||||
### Secure & Optimize PDFs
|
### Secure & Optimize PDFs
|
||||||
|
|
||||||
| Tool Name | Description |
|
| Tool Name | Description |
|
||||||
| :--- | :--- |
|
| :--------------------- | :----------------------------------------------------------------- |
|
||||||
| **Compress PDF** | Reduce file size while maintaining quality. |
|
| **Compress PDF** | Reduce file size while maintaining quality. |
|
||||||
| **Repair PDF** | Attempt to repair and recover data from a corrupted PDF. |
|
| **Repair PDF** | Attempt to repair and recover data from a corrupted PDF. |
|
||||||
| **Encrypt PDF** | Add a password to protect your PDF from unauthorized access. |
|
| **Encrypt PDF** | Add a password to protect your PDF from unauthorized access. |
|
||||||
@@ -73,6 +88,8 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
|||||||
| **Redact Content** | Permanently remove sensitive content from your PDFs. |
|
| **Redact Content** | Permanently remove sensitive content from your PDFs. |
|
||||||
| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). |
|
| **Edit Metadata** | View and modify PDF metadata (author, title, keywords, etc.). |
|
||||||
| **Remove Metadata** | Strip all metadata from your PDF for privacy. |
|
| **Remove Metadata** | Strip all metadata from your PDF for privacy. |
|
||||||
|
| **Linearize PDF** | Optimize PDF for fast web view. |
|
||||||
|
| **Sanitize PDF** | Remove potentially unwanted or malicous files from PDF. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -81,30 +98,134 @@ BentoPDF offers a comprehensive suite of tools to handle all your PDF needs.
|
|||||||
You can run BentoPDF locally for development or personal use.
|
You can run BentoPDF locally for development or personal use.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) (v18 or higher recommended)
|
- [Node.js](https://nodejs.org/) (v18 or higher recommended)
|
||||||
- [npm](https://www.npmjs.com/) (or yarn/pnpm)
|
- [npm](https://www.npmjs.com/) (or yarn/pnpm)
|
||||||
- [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/install/) (for containerized setup)
|
- [Docker](https://www.docker.com/) & [Docker Compose](https://docs.docker.com/compose/install/) (for containerized setup)
|
||||||
|
|
||||||
### 🚀 Run BentoPDF with Docker (Quick Start)
|
### 🚀 Quick Start with Docker
|
||||||
You can run BentoPDF directly from Docker Hub without cloning the repository:
|
|
||||||
|
|
||||||
|
[](https://zeabur.com/templates/LWO8I0?referralCode=LokiSalmonNeko)
|
||||||
|
|
||||||
|
You can run BentoPDF directly from Docker Hub or GitHub Container Registry without cloning the repository:
|
||||||
|
|
||||||
|
You can also watch the video on how to set it up 👉
|
||||||
|
[BentoPDF Docker Setup](https://drive.google.com/file/d/1C4eJ2nqeaH__1Tlad-xuBHaF2Ha4fSBf/view?usp=drive_link)
|
||||||
|
|
||||||
|
**Using Docker Hub:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull bentopdf/bentopdf:latest
|
docker run -p 3000:8080 bentopdf/bentopdf:latest
|
||||||
docker run -p 3000:80 bentopdf/bentopdf:latest
|
|
||||||
```
|
```
|
||||||
Open your browser at: http://localhost:3000.
|
|
||||||
|
**Using GitHub Container Registry:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Open your browser at: http://localhost:3000
|
||||||
|
|
||||||
This is the fastest way to try BentoPDF without setting up a development environment.
|
This is the fastest way to try BentoPDF without setting up a development environment.
|
||||||
|
|
||||||
### 🚀 Installation with npm
|
### 🚀 Run with Docker Compose (Recommended)
|
||||||
|
|
||||||
|
For a more robust setup with auto-restart capabilities:
|
||||||
|
|
||||||
|
1. **Download the repo and create a `docker-compose.yml` file or use the one given in repo**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
image: bentopdf/bentopdf:latest
|
||||||
|
container_name: bentopdf
|
||||||
|
ports:
|
||||||
|
- '3000:8080'
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start the application**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will be available at `http://localhost:3000`.
|
||||||
|
|
||||||
|
### 🏢 Simple Mode for Internal Use
|
||||||
|
|
||||||
|
For organizations that want a clean, distraction-free interface focused solely on PDF tools, BentoPDF supports a **Simple Mode** that hides all branding and marketing content.
|
||||||
|
|
||||||
|
**What Simple Mode does:**
|
||||||
|
|
||||||
|
- Hides navigation, hero section, features, FAQ, testimonials, and footer
|
||||||
|
- Shows only the essential PDF tools
|
||||||
|
- Updates page title to "PDF Tools"
|
||||||
|
- Perfect for internal company tools and educational institutions
|
||||||
|
|
||||||
|
For more details, see [SIMPLE_MODE.md](SIMPLE_MODE.md).
|
||||||
|
|
||||||
|
### 🔒 Security Features
|
||||||
|
|
||||||
|
BentoPDF runs as a non-root user using nginx-unprivileged for enhanced security:
|
||||||
|
|
||||||
|
- **Non-Root Execution**: Container runs with minimal privileges using nginx-unprivileged
|
||||||
|
- **Port 8080**: Uses high port number to avoid requiring root privileges
|
||||||
|
- **Security Best Practices**: Follows Principle of Least Privilege
|
||||||
|
|
||||||
|
#### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t bentopdf .
|
||||||
|
docker run -p 8080:8080 bentopdf
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed security configuration, see [SECURITY.md](SECURITY.md).
|
||||||
|
|
||||||
|
### 📦 Version Management
|
||||||
|
|
||||||
|
BentoPDF supports semantic versioning with multiple Docker tags available on both Docker Hub and GitHub Container Registry:
|
||||||
|
|
||||||
|
**Docker Hub:**
|
||||||
|
|
||||||
|
- **Latest**: `bentopdf/bentopdf:latest`
|
||||||
|
- **Specific Version**: `bentopdf/bentopdf:1.0.0`
|
||||||
|
- **Version with Prefix**: `bentopdf/bentopdf:v1.0.0`
|
||||||
|
|
||||||
|
**GitHub Container Registry:**
|
||||||
|
|
||||||
|
- **Latest**: `ghcr.io/alam00000/bentopdf:latest`
|
||||||
|
- **Specific Version**: `ghcr.io/alam00000/bentopdf:1.0.0`
|
||||||
|
- **Version with Prefix**: `ghcr.io/alam00000/bentopdf:v1.0.0`
|
||||||
|
|
||||||
|
#### Quick Release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Release a patch version (0.0.1 → 0.0.2)
|
||||||
|
npm run release
|
||||||
|
|
||||||
|
# Release a minor version (0.0.1 → 0.1.0)
|
||||||
|
npm run release:minor
|
||||||
|
|
||||||
|
# Release a major version (0.0.1 → 1.0.0)
|
||||||
|
npm run release:major
|
||||||
|
```
|
||||||
|
|
||||||
|
For detailed release instructions, see [RELEASE.md](RELEASE.md).
|
||||||
|
|
||||||
|
### 🚀 Development Setup
|
||||||
|
|
||||||
|
#### Option 1: Run with npm
|
||||||
|
|
||||||
1. **Clone the Repository**:
|
1. **Clone the Repository**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/alam00000/bentopdf.git
|
git clone https://github.com/alam00000/bentopdf.git
|
||||||
cd bentopdf
|
cd bentopdf
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install Dependencies**:
|
2. **Install Dependencies**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
@@ -115,31 +236,33 @@ This is the fastest way to try BentoPDF without setting up a development environ
|
|||||||
```
|
```
|
||||||
The application will be available at `http://localhost:5173`.
|
The application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
### 🚀 Running with Docker Compose Locally
|
#### Option 2: Build and Run with Docker Compose
|
||||||
|
|
||||||
1. **Clone the Repository**:
|
1. **Clone the Repository**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/alam00000/bentopdf.git
|
git clone https://github.com/alam00000/bentopdf.git
|
||||||
cd bentopdf
|
cd bentopdf
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run with Docker Compose**:
|
2. **Run with Docker Compose**:
|
||||||
Create a `docker-compose.yml` file with the content provided in the repository or use the existing one. Then, run:
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose -f docker-compose.dev.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The application will be available at `http://localhost:3000`.
|
The application will be available at `http://localhost:3000`.
|
||||||
|
|
||||||
> **Note:** After making any local changes to the code, it’s recommended to rebuild the Docker image using:
|
> **Note:** After making any local changes to the code, rebuild the Docker image using:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up --build -d
|
docker-compose -f docker-compose.dev.yml up --build -d
|
||||||
```
|
```
|
||||||
This ensures your latest changes are applied inside the container
|
|
||||||
|
This ensures your latest changes are applied inside the container.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 🛠️ Tech Stack & Background
|
## 🛠️ Tech Stack & Background
|
||||||
|
|
||||||
BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript**. As the project grew, it was migrated to a modern stack for better maintainability and scalability:
|
BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript**. As the project grew, it was migrated to a modern stack for better maintainability and scalability:
|
||||||
@@ -155,20 +278,21 @@ BentoPDF was originally built using **HTML**, **CSS**, and **vanilla JavaScript*
|
|||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|
||||||
### Planned Features:
|
### Planned Features:
|
||||||
|
|
||||||
- **HTML to PDF**: Convert HTML files or web pages into PDF documents.
|
- **HTML to PDF**: Convert HTML files or web pages into PDF documents.
|
||||||
- **Markdown to PDF**: Enhanced support for converting `.md` files to PDF.
|
- **Markdown to PDF**: Enhanced support for converting `.md` files to PDF.
|
||||||
- **Sanitize PDF**: Remove potentially malicious content like scripts from PDFs.
|
|
||||||
- **Convert to PDF/A**: Convert PDFs to the PDF/A archival format.
|
- **Convert to PDF/A**: Convert PDFs to the PDF/A archival format.
|
||||||
- **Edit PDF Content**: Directly edit text and other content within your PDF.
|
- **Edit PDF Content**: Directly edit text and other content within your PDF.
|
||||||
- **Linearize PDF**: Optimize PDFs for fast web viewing.
|
- **PDF to Office**: Converts PDF files into editable Word, Excel, and PowerPoint formats.
|
||||||
|
- **Office to PDF**: Converts Word, Excel, and PowerPoint documents into optimized PDFs.
|
||||||
|
|
||||||
Contributions and discussions on the roadmap are welcome! Join the conversation via [Discord](https://discord.gg/q42xWQmJ).
|
Contributions and discussions on the roadmap are welcome! Join the conversation via [Discord](https://discord.gg/AP2Y97juZT).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
We welcome contributions from the community! Here’s how you can get started:
|
We welcome contributions from the community! Here's how you can get started:
|
||||||
|
|
||||||
1. **Fork the repository** and create your branch from `main`.
|
1. **Fork the repository** and create your branch from `main`.
|
||||||
2. Follow the **Getting Started** steps to set up your local environment.
|
2. Follow the **Getting Started** steps to set up your local environment.
|
||||||
@@ -181,18 +305,20 @@ Have an idea for a new tool or an improvement? [Open an issue](https://github.co
|
|||||||
|
|
||||||
## Special Thanks
|
## Special Thanks
|
||||||
|
|
||||||
BentoPDF wouldn’t be possible without the amazing open-source tools and libraries that power it. We’d like to extend our heartfelt thanks to the creators and maintainers of:
|
BentoPDF wouldn't be possible without the amazing open-source tools and libraries that power it. We'd like to extend our heartfelt thanks to the creators and maintainers of:
|
||||||
|
|
||||||
- **[PDFLib.js](https://pdf-lib.js.org/)** – For enabling powerful client-side PDF manipulation.
|
- **[PDFLib.js](https://pdf-lib.js.org/)** – For enabling powerful client-side PDF manipulation.
|
||||||
- **[PDF.js](https://mozilla.github.io/pdf.js/)** – For the robust PDF rendering engine in the browser.
|
- **[PDF.js](https://mozilla.github.io/pdf.js/)** – For the robust PDF rendering engine in the browser.
|
||||||
- **[PDFKit](https://pdfkit.org/)** – For creating and editing PDFs with ease.
|
- **[PDFKit](https://pdfkit.org/)** – For creating and editing PDFs with ease.
|
||||||
- **[EmbedPDF](https://github.com/embedpdf/embed-pdf-viewer)** – For seamless PDF embedding in web pages.
|
- **[EmbedPDF](https://github.com/embedpdf/embed-pdf-viewer)** – For seamless PDF editing in pure JS.
|
||||||
- **[Cropper.js](https://fengyuanchen.github.io/cropperjs/)** – For intuitive image cropping functionality.
|
- **[Cropper.js](https://fengyuanchen.github.io/cropperjs/)** – For intuitive image cropping functionality.
|
||||||
- **[Vite](https://vitejs.dev/)** – For lightning-fast development and build tooling.
|
- **[Vite](https://vitejs.dev/)** – For lightning-fast development and build tooling.
|
||||||
- **[Tailwind CSS](https://tailwindcss.com/)** – For rapid, flexible, and beautiful UI styling.
|
- **[Tailwind CSS](https://tailwindcss.com/)** – For rapid, flexible, and beautiful UI styling.
|
||||||
|
- **[qpdf](https://github.com/qpdf/qpdf)** and **[qpdf-wasm](https://github.com/neslinesli93/qpdf-wasm)**– A powerful command-line tool and library for inspecting, repairing, and transforming PDF file ported to wasm
|
||||||
|
- **[cpdf](https://www.coherentpdf.com/)** – For content preserving pdf operations.
|
||||||
|
|
||||||
Your work inspires and empowers developers everywhere. Thank you for making open-source amazing!
|
Your work inspires and empowers developers everywhere. Thank you for making open-source amazing!
|
||||||
|
|
||||||
## 📜 License
|
## 📜 License
|
||||||
|
|
||||||
This project is licensed under the **Apache 2.0**. See the [LICENSE](https://github.com/alam00000/bentopdf/blob/main/LICENSE) file for details.
|
This project is licensed under the **GNU AGPLv3**. See the [LICENSE](https://github.com/alam00000/bentopdf/blob/main/LICENSE) file for details.
|
||||||
+317
@@ -0,0 +1,317 @@
|
|||||||
|
# 🚀 Release Process - Real-World Scenarios
|
||||||
|
|
||||||
|
## 📋 Common Release Scenarios
|
||||||
|
|
||||||
|
### **Scenario 1: I Just Finished a Feature and Want to Release**
|
||||||
|
|
||||||
|
**Situation:** You've completed a new feature, tested it locally, and want to release it.
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git status
|
||||||
|
On branch main
|
||||||
|
Your branch is up to date with 'origin/main'.
|
||||||
|
|
||||||
|
Changes not staged for commit:
|
||||||
|
modified: src/js/new-feature.js
|
||||||
|
modified: src/css/styles.css
|
||||||
|
modified: README.md
|
||||||
|
|
||||||
|
Untracked files:
|
||||||
|
src/js/feature-helper.js
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Commit your feature changes
|
||||||
|
git add .
|
||||||
|
git commit -m "Add new PDF watermark feature"
|
||||||
|
|
||||||
|
# 2. Choose your release type and run
|
||||||
|
npm run release # Patch: 1.0.0 → 1.0.1 (bug fixes, small improvements)
|
||||||
|
npm run release:minor # Minor: 1.0.0 → 1.1.0 (new features, backward compatible)
|
||||||
|
npm run release:major # Major: 1.0.0 → 2.0.0 (breaking changes)
|
||||||
|
```
|
||||||
|
|
||||||
|
**What Happens:**
|
||||||
|
|
||||||
|
- ✅ Your feature commit stays as-is
|
||||||
|
- ✅ Version gets bumped in `package.json`
|
||||||
|
- ✅ New release commit is created
|
||||||
|
- ✅ Git tag is created (e.g., `v1.0.1`)
|
||||||
|
- ✅ Everything gets pushed to GitHub
|
||||||
|
- ✅ Docker image gets built and published
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 2: I Have Uncommitted Changes and Want to Release**
|
||||||
|
|
||||||
|
**Situation:** You have local changes but haven't committed them yet.
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ git status
|
||||||
|
Changes not staged for commit:
|
||||||
|
modified: package.json
|
||||||
|
modified: src/js/main.js
|
||||||
|
modified: README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ This Will Fail:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run release
|
||||||
|
# Error: Your local changes would be overwritten by merge
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Solution Options:**
|
||||||
|
|
||||||
|
**Option A: Commit Everything First (Recommended)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Add new features and improvements"
|
||||||
|
npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Stash Changes Temporarily**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git stash
|
||||||
|
npm run release
|
||||||
|
git stash pop # Restore your changes after release
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option C: Commit Only What's Needed**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add package.json src/js/main.js
|
||||||
|
git commit -m "Add core improvements"
|
||||||
|
npm run release
|
||||||
|
git add README.md
|
||||||
|
git commit -m "Update documentation"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 3: I Want to Release a Hotfix**
|
||||||
|
|
||||||
|
**Situation:** There's a critical bug in production that needs immediate fixing.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Fix the bug
|
||||||
|
git add src/js/bug-fix.js
|
||||||
|
git commit -m "Fix critical PDF rendering issue"
|
||||||
|
|
||||||
|
# 2. Release as patch (bug fix)
|
||||||
|
npm run release
|
||||||
|
# This creates: 1.0.0 → 1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
|
||||||
|
- ✅ Bug fix gets released immediately
|
||||||
|
- ✅ Docker image with fix is available
|
||||||
|
- ✅ Users can pull the fixed version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 4: I Want to Release a Major Update**
|
||||||
|
|
||||||
|
**Situation:** You've added significant new features that might break existing functionality.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Commit all your changes
|
||||||
|
git add .
|
||||||
|
git commit -m "Add major PDF editing features and API changes"
|
||||||
|
|
||||||
|
# 2. Release as major version
|
||||||
|
npm run release:major
|
||||||
|
# This creates: 1.0.0 → 2.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:**
|
||||||
|
|
||||||
|
- ✅ Major version bump indicates breaking changes
|
||||||
|
- ✅ Users know to check compatibility
|
||||||
|
- ✅ Both old and new versions available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 5: I Want to Release Multiple Features at Once**
|
||||||
|
|
||||||
|
**Situation:** You've been working on multiple features and want to release them together.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Commit all features
|
||||||
|
git add .
|
||||||
|
git commit -m "Add multiple PDF tools: watermark, encryption, and compression"
|
||||||
|
|
||||||
|
# 2. Choose appropriate release type
|
||||||
|
npm run release:minor # For new features (1.0.0 → 1.1.0)
|
||||||
|
# OR
|
||||||
|
npm run release:major # For breaking changes (1.0.0 → 2.0.0)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **Scenario 6: I Want to Test the Release Process**
|
||||||
|
|
||||||
|
**Situation:** You want to test the release system without affecting production.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Make a small test change
|
||||||
|
echo "// Test comment" >> src/js/main.js
|
||||||
|
git add src/js/main.js
|
||||||
|
git commit -m "Test release process"
|
||||||
|
|
||||||
|
# 2. Run patch release
|
||||||
|
npm run release
|
||||||
|
# This creates: 1.0.0 → 1.0.1
|
||||||
|
|
||||||
|
# 3. Verify everything works
|
||||||
|
# Check GitHub Actions, Docker Hub, etc.
|
||||||
|
|
||||||
|
# 4. If you want to undo the test release
|
||||||
|
git tag -d v1.0.1
|
||||||
|
git push origin :refs/tags/v1.0.1
|
||||||
|
git reset --hard HEAD~1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Release Type Guidelines**
|
||||||
|
|
||||||
|
| Scenario | Command | Version Change | When to Use |
|
||||||
|
| ------------------- | ----------------------- | --------------- | ------------------------------------ |
|
||||||
|
| **Bug Fix** | `npm run release` | `1.0.0 → 1.0.1` | Fixing bugs, small improvements |
|
||||||
|
| **New Feature** | `npm run release:minor` | `1.0.0 → 1.1.0` | Adding features, backward compatible |
|
||||||
|
| **Breaking Change** | `npm run release:major` | `1.0.0 → 2.0.0` | API changes, major rewrites |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 **What Happens After You Run a Release Command**
|
||||||
|
|
||||||
|
### **Immediate Actions (Local):**
|
||||||
|
|
||||||
|
1. **Version Update**: `package.json` version gets bumped
|
||||||
|
2. **Git Commit**: New commit created with "Release vX.X.X"
|
||||||
|
3. **Git Tag**: Tag created (e.g., `v1.0.1`)
|
||||||
|
4. **Git Push**: Everything pushed to GitHub
|
||||||
|
|
||||||
|
### **Automatic Actions (GitHub):**
|
||||||
|
|
||||||
|
1. **GitHub Actions Triggered**: Workflow starts building Docker image
|
||||||
|
2. **Docker Build**: Multi-architecture image created
|
||||||
|
3. **Docker Push**: Images pushed to Docker Hub with tags:
|
||||||
|
- `bentopdf/bentopdf:latest`
|
||||||
|
- `bentopdf/bentopdf:1.0.1`
|
||||||
|
- `bentopdf/bentopdf:v1.0.1`
|
||||||
|
|
||||||
|
### **End Result:**
|
||||||
|
|
||||||
|
Users can immediately pull your new version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull bentopdf/bentopdf:1.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 **Before You Release - Prerequisites**
|
||||||
|
|
||||||
|
### **1. Docker Hub Credentials Setup**
|
||||||
|
|
||||||
|
You need to add these secrets to your GitHub repository:
|
||||||
|
|
||||||
|
1. Go to **Settings** → **Secrets and variables** → **Actions**
|
||||||
|
2. Add these secrets:
|
||||||
|
- `DOCKER_USERNAME`: Your Docker Hub username
|
||||||
|
- `DOCKER_TOKEN`: Your Docker Hub access token
|
||||||
|
|
||||||
|
### **2. Get Docker Hub Token**
|
||||||
|
|
||||||
|
1. Go to [Docker Hub](https://hub.docker.com)
|
||||||
|
2. Account Settings → Security → New Access Token
|
||||||
|
3. Set permissions to "Read, Write, Delete"
|
||||||
|
4. Copy the token and add it to GitHub Secrets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 **Troubleshooting Common Issues**
|
||||||
|
|
||||||
|
### **❌ "Your local changes would be overwritten by merge"**
|
||||||
|
|
||||||
|
**Problem:** You have uncommitted changes
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Your commit message"
|
||||||
|
npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
### **❌ "Permission denied" in GitHub Actions**
|
||||||
|
|
||||||
|
**Problem:** Missing Docker Hub credentials
|
||||||
|
**Solution:** Add `DOCKER_USERNAME` and `DOCKER_TOKEN` to GitHub Secrets
|
||||||
|
|
||||||
|
### **❌ "Tag already exists"**
|
||||||
|
|
||||||
|
**Problem:** You've run the same release before
|
||||||
|
**Solution:** This is normal! The script will skip creating duplicate tags
|
||||||
|
|
||||||
|
### **❌ GitHub Actions fails**
|
||||||
|
|
||||||
|
**Problem:** Various build issues
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
1. Check Actions tab for detailed logs
|
||||||
|
2. Verify Docker Hub credentials
|
||||||
|
3. Check Dockerfile for syntax errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 **Testing Your Release System**
|
||||||
|
|
||||||
|
### **Quick Test:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Make a small change
|
||||||
|
echo "// Test" >> src/js/main.js
|
||||||
|
git add src/js/main.js
|
||||||
|
git commit -m "Test release"
|
||||||
|
npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Verify Results:**
|
||||||
|
|
||||||
|
1. **GitHub Actions**: Check Actions tab for successful build
|
||||||
|
2. **Docker Hub**: Verify images are published
|
||||||
|
3. **Git Tags**: `git tag --list` should show new tag
|
||||||
|
4. **Version**: `cat package.json | grep version` should show updated version
|
||||||
|
|
||||||
|
### **Undo Test Release:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -d v1.0.1
|
||||||
|
git push origin :refs/tags/v1.0.1
|
||||||
|
git reset --hard HEAD~1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 **That's It!**
|
||||||
|
|
||||||
|
Your release system is now ready! Just follow the scenarios above based on your situation and run the appropriate `npm run release` command.
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
# Security Configuration
|
||||||
|
|
||||||
|
## Non-Root User Support
|
||||||
|
|
||||||
|
BentoPDF now uses nginx-unprivileged for enhanced security. This follows the Principle of Least Privilege and is essential for production environments.
|
||||||
|
|
||||||
|
### Security Benefits
|
||||||
|
|
||||||
|
- **Reduced Attack Surface**: If compromised, attackers won't have root privileges
|
||||||
|
- **Compliance**: Meets security standards like SOC 2, PCI DSS
|
||||||
|
- **Kubernetes/OpenShift Compatibility**: Works with security policies that require non-root execution
|
||||||
|
- **System Protection**: Prevents system-wide damage if the application is compromised
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
#### Default Configuration (nginx-unprivileged)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t bentopdf .
|
||||||
|
docker run -p 8080:8080 bentopdf
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Simple Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with simple mode enabled
|
||||||
|
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 8080:8080 bentopdf-simple
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Kubernetes Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: bentopdf
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 2000
|
||||||
|
runAsGroup: 2000
|
||||||
|
containers:
|
||||||
|
- name: bentopdf
|
||||||
|
image: bentopdf:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Compose Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
SIMPLE_MODE: false
|
||||||
|
ports:
|
||||||
|
- '8080:8080'
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
To verify the container is running as non-root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check the user inside the container
|
||||||
|
docker exec <container_id> whoami
|
||||||
|
# Should output: nginx
|
||||||
|
|
||||||
|
# Check the user ID
|
||||||
|
docker exec <container_id> id
|
||||||
|
# Should show UID/GID for nginx user (typically 101)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Best Practices
|
||||||
|
|
||||||
|
1. **Use nginx-unprivileged**: Built-in non-root user with minimal privileges
|
||||||
|
2. **Regular Updates**: Keep the base image updated (currently using 1.29-alpine)
|
||||||
|
3. **Port 8080**: Use high port numbers to avoid requiring root privileges
|
||||||
|
4. **Security Scanning**: Regularly scan images for vulnerabilities
|
||||||
|
5. **Network Policies**: Implement network segmentation
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
If you encounter permission issues:
|
||||||
|
|
||||||
|
1. **Check file ownership**: Ensure all application files are owned by the nginx user
|
||||||
|
2. **Verify PID directory**: Ensure `/etc/nginx/tmp/` directory exists and is writable
|
||||||
|
3. **Port binding**: Ensure port 8080 is available and not blocked by firewall
|
||||||
|
|
||||||
|
### Migration from Root
|
||||||
|
|
||||||
|
If migrating from a root-based setup:
|
||||||
|
|
||||||
|
1. Update your Dockerfile to use nginx-unprivileged base image
|
||||||
|
2. Change port mappings from 80 to 8080 in all configurations
|
||||||
|
3. Update nginx.conf to use `/etc/nginx/tmp/nginx.pid` for PID file
|
||||||
|
4. Rebuild your images with the new security settings
|
||||||
|
5. Update your deployment configurations (Kubernetes, Docker Compose, etc.)
|
||||||
|
6. Test thoroughly in a staging environment
|
||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
# Simple Mode for BentoPDF
|
||||||
|
|
||||||
|
Simple Mode is designed for internal organizational use where you want to hide all branding and marketing content, showing only the essential PDF tools for your users.
|
||||||
|
|
||||||
|
## What Simple Mode Does
|
||||||
|
|
||||||
|
When enabled, Simple Mode will:
|
||||||
|
|
||||||
|
- Hide the navigation bar
|
||||||
|
- Hide the hero section with marketing content
|
||||||
|
- Hide the features section
|
||||||
|
- Hide the security/compliance section
|
||||||
|
- Hide the FAQ section
|
||||||
|
- Hide the testimonials section
|
||||||
|
- Hide the support section
|
||||||
|
- Hide the footer
|
||||||
|
- Update the page title to "PDF Tools"
|
||||||
|
- Make the tools section more prominent
|
||||||
|
|
||||||
|
## How to Enable Simple Mode
|
||||||
|
|
||||||
|
### Method 1: Using Pre-built Simple Mode Image (Recommended)
|
||||||
|
|
||||||
|
Use the pre-built Simple Mode image directly:
|
||||||
|
|
||||||
|
**Using Docker Hub:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 3000:8080 bentopdf/bentopdf-simple:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using GitHub Container Registry:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 3000:8080 ghcr.io/alam00000/bentopdf-simple:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with Docker Compose:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
# Using Docker Hub
|
||||||
|
image: bentopdf/bentopdf-simple:latest
|
||||||
|
# Or using GitHub Container Registry
|
||||||
|
# image: ghcr.io/alam00000/bentopdf-simple:latest
|
||||||
|
container_name: bentopdf
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '3000:8080'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Using Docker Compose with Build
|
||||||
|
|
||||||
|
Build the image locally with Simple Mode enabled:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml build --build-arg SIMPLE_MODE=true
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 3: Using Docker Build
|
||||||
|
|
||||||
|
Build the image with the SIMPLE_MODE build argument:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --build-arg SIMPLE_MODE=true -t bentopdf-simple .
|
||||||
|
docker run -p 3000:8080 bentopdf-simple
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 4: Using npm Script (Easiest for Local Development)
|
||||||
|
|
||||||
|
Use the built-in npm script that handles everything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run serve:simple
|
||||||
|
```
|
||||||
|
|
||||||
|
This command automatically:
|
||||||
|
|
||||||
|
- Sets `SIMPLE_MODE=true`
|
||||||
|
- Builds the project with Simple Mode enabled
|
||||||
|
- Serves the built files on `http://localhost:3000`
|
||||||
|
|
||||||
|
### Method 5: Using Environment Variables
|
||||||
|
|
||||||
|
Set the environment variable before building:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export SIMPLE_MODE=true
|
||||||
|
npm run build
|
||||||
|
npx serve dist -p 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing Simple Mode Locally
|
||||||
|
|
||||||
|
### Method 1: Using npm Script (Easiest for Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run serve:simple
|
||||||
|
```
|
||||||
|
|
||||||
|
This automatically builds and serves Simple Mode on `http://localhost:3000`.
|
||||||
|
|
||||||
|
### Method 2: Using Pre-built Image (Easiest for Production)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull and run the Simple Mode image
|
||||||
|
docker pull bentopdf/bentopdf-simple:latest
|
||||||
|
docker run -p 3000:8080 bentopdf/bentopdf-simple:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
|
### Method 3: Build and Test Locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build with simple mode
|
||||||
|
SIMPLE_MODE=true npm run build
|
||||||
|
|
||||||
|
# Serve the built files
|
||||||
|
npx serve dist -p 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:3000` in your browser.
|
||||||
|
|
||||||
|
### Method 4: Compare Both Modes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test Normal Mode
|
||||||
|
docker run -p 3000:8080 bentopdf/bentopdf:latest
|
||||||
|
|
||||||
|
# Test Simple Mode
|
||||||
|
docker run -p 3001:8080 bentopdf/bentopdf-simple:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
- Normal Mode: `http://localhost:3000`
|
||||||
|
- Simple Mode: `http://localhost:3001`
|
||||||
|
|
||||||
|
## 🔍 What to Look For
|
||||||
|
|
||||||
|
When Simple Mode is working correctly, you should see:
|
||||||
|
|
||||||
|
- ✅ Clean "PDF Tools" header (no marketing hero section)
|
||||||
|
- ✅ "Select a tool to get started" subtitle
|
||||||
|
- ✅ Search bar for tools
|
||||||
|
- ✅ All PDF tool cards organized by category
|
||||||
|
- ❌ No navigation bar
|
||||||
|
- ❌ No hero section with "The PDF Toolkit built for privacy"
|
||||||
|
- ❌ No features, FAQ, testimonials, or footer sections
|
||||||
|
|
||||||
|
## 📦 Available Docker Images
|
||||||
|
|
||||||
|
### Normal Mode (Full Branding)
|
||||||
|
|
||||||
|
**Docker Hub:**
|
||||||
|
|
||||||
|
- `bentopdf/bentopdf:latest`
|
||||||
|
- `bentopdf/bentopdf:v1.0.0` (versioned)
|
||||||
|
|
||||||
|
**GitHub Container Registry:**
|
||||||
|
|
||||||
|
- `ghcr.io/alam00000/bentopdf:latest`
|
||||||
|
- `ghcr.io/alam00000/bentopdf:v1.0.0` (versioned)
|
||||||
|
|
||||||
|
### Simple Mode (Clean Interface)
|
||||||
|
|
||||||
|
**Docker Hub:**
|
||||||
|
|
||||||
|
- `bentopdf/bentopdf-simple:latest`
|
||||||
|
- `bentopdf/bentopdf-simple:v1.0.0` (versioned)
|
||||||
|
|
||||||
|
**GitHub Container Registry:**
|
||||||
|
|
||||||
|
- `ghcr.io/alam00000/bentopdf-simple:latest`
|
||||||
|
- `ghcr.io/alam00000/bentopdf-simple:v1.0.0` (versioned)
|
||||||
|
|
||||||
|
## 🚀 Production Deployment Examples
|
||||||
|
|
||||||
|
### Internal Company Tool
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
image: bentopdf/bentopdf-simple:latest
|
||||||
|
container_name: bentopdf
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '80:80'
|
||||||
|
environment:
|
||||||
|
- PUID=1000
|
||||||
|
- PGID=1000
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ Important Notes
|
||||||
|
|
||||||
|
- **Pre-built images**: Use `bentopdf/bentopdf-simple:latest` for Simple Mode
|
||||||
|
- **Environment variables**: `SIMPLE_MODE=true` only works during build, not runtime
|
||||||
|
- **Build-time optimization**: Simple Mode uses dead code elimination for smaller bundles
|
||||||
|
- **Same functionality**: All PDF tools work identically in both modes
|
||||||
+231
-84
@@ -1,25 +1,31 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>About Bentopdf - Fast, Private, and Free PDF Tools</title>
|
<title>About Bentopdf - Fast, Private, and Free PDF Tools</title>
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
<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>
|
</head>
|
||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bentopdf Logo" class="h-8 w-8 mr-2">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
id="home-logo"
|
||||||
</a>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
@@ -27,56 +33,132 @@
|
|||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Mobile Hamburger Button -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="md:hidden flex items-center">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
|
|
||||||
<section id="about-hero" class="text-center py-16 md:py-24">
|
<section id="about-hero" class="text-center py-16 md:py-24">
|
||||||
<h1 class="text-3xl md:text-6xl font-bold text-white mb-4">
|
<h1 class="text-3xl md:text-6xl font-bold text-white mb-4">
|
||||||
We believe PDF tools should be <span class="marker-slanted">fast, private, and free.</span>
|
We believe PDF tools should be
|
||||||
|
<span class="marker-slanted">fast, private, and free.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg md:text-xl text-gray-400">
|
<p class="text-lg md:text-xl text-gray-400">No compromises.</p>
|
||||||
No compromises.
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="section-divider"></div>
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
|
<section id="mission-section" class="py-16 max-w-4xl mx-auto">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<i data-lucide="rocket" class="w-16 h-16 text-indigo-400 mx-auto mb-6"></i>
|
<i
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">Our Mission</h2>
|
data-lucide="rocket"
|
||||||
|
class="w-16 h-16 text-indigo-400 mx-auto mb-6"
|
||||||
|
></i>
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-white mb-4">
|
||||||
|
Our Mission
|
||||||
|
</h2>
|
||||||
<p class="text-lg text-gray-400 leading-relaxed">
|
<p class="text-lg text-gray-400 leading-relaxed">
|
||||||
To provide the most comprehensive PDF toolbox that respects your privacy and never asks for payment.
|
To provide the most comprehensive PDF toolbox that respects your
|
||||||
We believe essential document tools should be accessible to everyone, everywhere, without barriers.
|
privacy and never asks for payment. We believe essential document
|
||||||
|
tools should be accessible to everyone, everywhere, without
|
||||||
|
barriers.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700">
|
<div
|
||||||
|
class="bg-gray-800 rounded-xl p-8 md:p-12 my-16 border border-gray-700"
|
||||||
|
>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
|
||||||
<div class="text-center md:text-left">
|
<div class="text-center md:text-left">
|
||||||
<span class="text-indigo-400 font-bold uppercase">Our Core Philosophy</span>
|
<span class="text-indigo-400 font-bold uppercase"
|
||||||
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">Privacy First. Always.</h2>
|
>Our Core Philosophy</span
|
||||||
|
>
|
||||||
|
<h2 class="text-3xl md:text-4xl font-bold text-white mt-2 mb-4">
|
||||||
|
Privacy First. Always.
|
||||||
|
</h2>
|
||||||
<p class="text-gray-400 leading-relaxed">
|
<p class="text-gray-400 leading-relaxed">
|
||||||
In an era where data is a commodity, we take a different approach. All processing for Bentopdf
|
In an era where data is a commodity, we take a different approach.
|
||||||
tools happens locally in your browser. This means your files never touch our servers, we never
|
All processing for Bentopdf tools happens locally in your browser.
|
||||||
see your documents, and we don't track what you do. Your documents remain completely and
|
This means your files never touch our servers, we never see your
|
||||||
unequivocally private. It's not just a feature; it's our foundation.
|
documents, and we don't track what you do. Your documents remain
|
||||||
|
completely and unequivocally private. It's not just a feature;
|
||||||
|
it's our foundation.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="relative w-48 h-48">
|
<div class="relative w-48 h-48">
|
||||||
<div class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"></div>
|
<div
|
||||||
<div class="absolute inset-4 bg-indigo-500 rounded-full opacity-30 animate-pulse delay-500">
|
class="absolute inset-0 bg-indigo-500 rounded-full opacity-20 animate-pulse"
|
||||||
</div>
|
></div>
|
||||||
<i data-lucide="shield-check" class="w-48 h-48 text-indigo-400"></i>
|
<div
|
||||||
|
class="absolute inset-4 bg-indigo-500 rounded-full opacity-30 animate-pulse delay-500"
|
||||||
|
></div>
|
||||||
|
<i
|
||||||
|
data-lucide="shield-check"
|
||||||
|
class="w-48 h-48 text-indigo-400"
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,39 +170,60 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-5xl mx-auto">
|
||||||
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
||||||
<i data-lucide="zap" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
<i
|
||||||
|
data-lucide="zap"
|
||||||
|
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
||||||
|
></i>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-white">Built for Speed</h3>
|
<h3 class="text-xl font-bold text-white">Built for Speed</h3>
|
||||||
<p class="text-gray-400 mt-2">No waiting for uploads or downloads to a server. By processing
|
<p class="text-gray-400 mt-2">
|
||||||
files directly in your browser using modern web technologies like WebAssembly, we offer
|
No waiting for uploads or downloads to a server. By processing
|
||||||
unparalleled speed for all our tools.</p>
|
files directly in your browser using modern web technologies
|
||||||
</div>
|
like WebAssembly, we offer unparalleled speed for all our tools.
|
||||||
</div>
|
|
||||||
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
|
||||||
<i data-lucide="badge-dollar-sign" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white">Completely Free</h3>
|
|
||||||
<p class="text-gray-400 mt-2">No trials, no subscriptions, no hidden fees, and no "premium"
|
|
||||||
features held hostage. We believe powerful PDF tools should be a public utility, not a
|
|
||||||
profit center.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
|
||||||
<i data-lucide="user-plus" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-xl font-bold text-white">No Account Required</h3>
|
|
||||||
<p class="text-gray-400 mt-2">Start using any tool immediately. We don't need your email, a
|
|
||||||
password, or any personal information. Your workflow should be frictionless and anonymous.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
||||||
<i data-lucide="code-2" class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"></i>
|
<i
|
||||||
|
data-lucide="badge-dollar-sign"
|
||||||
|
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
||||||
|
></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white">Completely Free</h3>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
No trials, no subscriptions, no hidden fees, and no "premium"
|
||||||
|
features held hostage. We believe powerful PDF tools should be a
|
||||||
|
public utility, not a profit center.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
||||||
|
<i
|
||||||
|
data-lucide="user-plus"
|
||||||
|
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
||||||
|
></i>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-xl font-bold text-white">No Account Required</h3>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
Start using any tool immediately. We don't need your email, a
|
||||||
|
password, or any personal information. Your workflow should be
|
||||||
|
frictionless and anonymous.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-800 p-6 rounded-lg flex items-start gap-4">
|
||||||
|
<i
|
||||||
|
data-lucide="code-2"
|
||||||
|
class="w-10 h-10 text-indigo-400 flex-shrink-0 mt-1"
|
||||||
|
></i>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-white">Open Source Spirit</h3>
|
<h3 class="text-xl font-bold text-white">Open Source Spirit</h3>
|
||||||
<p class="text-gray-400 mt-2">Built with transparency in mind. We leverage incredible
|
<p class="text-gray-400 mt-2">
|
||||||
open-source libraries like PDF-lib and PDF.js, and believe in the community-driven effort to
|
Built with transparency in mind. We leverage incredible
|
||||||
make powerful tools accessible to everyone.</p>
|
open-source libraries like PDF-lib and PDF.js, and believe in
|
||||||
|
the community-driven effort to make powerful tools accessible to
|
||||||
|
everyone.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,60 +232,105 @@
|
|||||||
<div class="section-divider"></div>
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
<section id="cta-section" class="text-center py-16">
|
<section id="cta-section" class="text-center py-16">
|
||||||
<h2 class="text-3xl font-bold text-white mb-4">Ready to get started?</h2>
|
<h2 class="text-3xl font-bold text-white mb-4">
|
||||||
|
Ready to get started?
|
||||||
|
</h2>
|
||||||
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||||
Join thousands of users who trust Bentopdf for their daily document needs. Experience the difference
|
Join thousands of users who trust Bentopdf for their daily document
|
||||||
that privacy and performance can make.
|
needs. Experience the difference that privacy and performance can
|
||||||
|
make.
|
||||||
</p>
|
</p>
|
||||||
<a href="index.html#tools-header"
|
<a
|
||||||
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1">
|
href="index.html#tools-header"
|
||||||
|
class="inline-block px-8 py-3 rounded-full bg-gradient-to-b from-indigo-500 to-indigo-600 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:shadow-xl hover:shadow-indigo-500/30 transition-all duration-200 transform hover:-translate-y-1"
|
||||||
|
>
|
||||||
Explore All Tools
|
Explore All Tools
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
|
>
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img src="images/favicon.svg" alt="Bentopdf Logo" class="h-10 w-10 mr-3">
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bentopdf Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
|
/>
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="./about.html" class="hover:text-indigo-400">About Us</a></li>
|
<li>
|
||||||
<li><a href="./faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
<a href="./about.html" class="hover:text-indigo-400"
|
||||||
<li><a href="./contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
>About Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./faq.html" class="hover:text-indigo-400">FAQ</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
<li>
|
||||||
<li><a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
<a href="./terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="instagram"></i>
|
<i data-lucide="instagram"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.linkedin.com/company/bentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="linkedin"></i>
|
<i data-lucide="linkedin"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
href="https://x.com/BentoPDF"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,9 +339,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
lucide.createIcons();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+141
-36
@@ -1,23 +1,31 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Contact Us - BentoPDF</title>
|
<title>Contact Us - BentoPDF</title>
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
<link rel="icon" type="image/png" href="images/favicon.svg">
|
|
||||||
<link href="/src/css/styles.css" rel="stylesheet">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl"> BentoPDF</span>
|
id="home-logo"
|
||||||
</a>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
@@ -25,72 +33,170 @@
|
|||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Mobile Hamburger Button -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="md:hidden flex items-center">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
<section id="contact-hero" class="text-center py-16 md:py-24">
|
<section id="contact-hero" class="text-center py-16 md:py-24">
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">Get in Touch</h1>
|
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||||
|
Get in Touch
|
||||||
|
</h1>
|
||||||
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
||||||
We'd love to hear from you. Whether you have a question, feedback, or a feature request, please don't
|
We'd love to hear from you. Whether you have a question, feedback, or
|
||||||
hesitate to reach out.
|
a feature request, please don't hesitate to reach out.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="max-w-2xl mx-auto text-center py-8">
|
<div class="max-w-2xl mx-auto text-center py-8">
|
||||||
<p class="text-lg text-gray-400">
|
<p class="text-lg text-gray-400">
|
||||||
You can reach us directly by email at:
|
You can reach us directly by email at:
|
||||||
<a href="mailto:contact@bentopdf.com"
|
<a
|
||||||
class="text-indigo-400 underline hover:text-indigo-300">contact@bentopdf.com</a>
|
href="mailto:contact@bentopdf.com"
|
||||||
|
class="text-indigo-400 underline hover:text-indigo-300"
|
||||||
|
>contact@bentopdf.com</a
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
|
>
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3">
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
|
/>
|
||||||
<span class="text-xl font-bold text-white">Bentopdf</span>
|
<span class="text-xl font-bold text-white">Bentopdf</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">© 2025 Bentopdf. All rights reserved.</p>
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 Bentopdf. All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="about.html" class="hover:text-indigo-400">About Us</a></li>
|
<li>
|
||||||
|
<a href="about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||||
<li><a href="contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
<li>
|
||||||
|
<a href="contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
<li>
|
||||||
<li><a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
<a href="./terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="instagram"></i>
|
<i data-lucide="instagram"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.linkedin.com/company/bentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="linkedin"></i>
|
<i data-lucide="linkedin"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
href="https://x.com/BentoPDF"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,9 +206,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
lucide.createIcons();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
bentopdf:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
SIMPLE_MODE: false # false for default mode, true for simple mode
|
||||||
|
container_name: bentopdf
|
||||||
|
ports:
|
||||||
|
- '8080:8080'
|
||||||
|
restart: unless-stopped
|
||||||
+5
-7
@@ -1,11 +1,9 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
bentopdf:
|
bentopdf:
|
||||||
build:
|
# simple mode - bentopdf/bentopdf-simple:latest
|
||||||
context: .
|
# default mode - bentopdf/bentopdf:latest
|
||||||
dockerfile: Dockerfile
|
image: bentopdf/bentopdf-simple:latest
|
||||||
container_name: bentopdf
|
container_name: bentopdf
|
||||||
ports:
|
|
||||||
- "3000:80"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '8080:8080'
|
||||||
@@ -1,26 +1,31 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Frequently Asked Questions - BentoPDF</title>
|
<title>Frequently Asked Questions - BentoPDF</title>
|
||||||
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
<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>
|
</head>
|
||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer" id="home-logo">
|
<div
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-8 w-8 mr-2">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
id="home-logo"
|
||||||
</a>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
@@ -28,116 +33,257 @@
|
|||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Mobile Hamburger Button -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="md:hidden flex items-center">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
<div id="app" class="min-h-screen container mx-auto p-4 md:p-8">
|
||||||
|
|
||||||
<section id="faq-hero" class="text-center py-16 md:py-24">
|
<section id="faq-hero" class="text-center py-16 md:py-24">
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||||
Frequently Asked Questions
|
Frequently Asked Questions
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
<p class="text-lg md:text-xl text-gray-400 max-w-3xl mx-auto">
|
||||||
Have questions? We've got answers. Here are some of the most common things people ask about BentoPDF.
|
Have questions? We've got answers. Here are some of the most common
|
||||||
|
things people ask about BentoPDF.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto space-y-4">
|
<div class="max-w-4xl mx-auto space-y-4">
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Are my files safe and private?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Are my files safe and private?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p><strong>Absolutely.</strong> This is the most important feature of BentoPDF. All processing
|
<p>
|
||||||
happens 100% locally in your web browser. Your files are never uploaded to any server, which
|
<strong>Absolutely.</strong> This is the most important feature of
|
||||||
means we—or anyone else—can never see them. Your privacy is guaranteed.</p>
|
BentoPDF. All processing happens 100% locally in your web browser.
|
||||||
|
Your files are never uploaded to any server, which means we—or
|
||||||
|
anyone else—can never see them. Your privacy is guaranteed.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Is BentoPDF really free? What's the catch?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Is BentoPDF really free? What's the catch?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>Yes, it's completely free, and there's no catch. There are no hidden fees, no subscription plans,
|
<p>
|
||||||
no usage limits, and no premium-only features. We believe essential tools should be accessible
|
Yes, it's completely free, and there's no catch. There are no
|
||||||
to everyone.</p>
|
hidden fees, no subscription plans, no usage limits, and no
|
||||||
|
premium-only features. We believe essential tools should be
|
||||||
|
accessible to everyone.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Do I need an internet connection to use the tools?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Do I need an internet connection to use the tools?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>After you load the website for the first time, most tools will work completely offline! Because
|
<p>
|
||||||
all the processing libraries are loaded into your browser, you can disconnect from the internet
|
After you load the website for the first time, most tools will
|
||||||
and continue to merge, split, compress, and edit your PDFs securely. Some niche tools that
|
work completely offline! Because all the processing libraries are
|
||||||
require external data (like Markdown to PDF with web images) may need a connection.</p>
|
loaded into your browser, you can disconnect from the internet and
|
||||||
|
continue to merge, split, compress, and edit your PDFs securely.
|
||||||
|
Some niche tools that require external data (like Markdown to PDF
|
||||||
|
with web images) may need a connection.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Are there any file size or usage limitations?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Are there any file size or usage limitations?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>No, we do not impose any artificial limits on file size, the number of files, or how many times
|
<p>
|
||||||
you can use a tool. The only practical limitation is the processing power and memory of your own
|
No, we do not impose any artificial limits on file size, the
|
||||||
computer, as very large or complex files may take longer to process.</p>
|
number of files, or how many times you can use a tool. The only
|
||||||
|
practical limitation is the processing power and memory of your
|
||||||
|
own computer, as very large or complex files may take longer to
|
||||||
|
process.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Why did my PDF fail to process?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Why did my PDF fail to process?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>Failures are rare but can happen for a few reasons:</p>
|
<p>Failures are rare but can happen for a few reasons:</p>
|
||||||
<ul class="list-disc list-inside mt-2 space-y-1">
|
<ul class="list-disc list-inside mt-2 space-y-1">
|
||||||
<li>The PDF might be corrupted or not compliant with standard specifications.</li>
|
<li>
|
||||||
<li>The file could be encrypted with a password you don't have. Please use our Decrypt tool
|
The PDF might be corrupted or not compliant with standard
|
||||||
first.</li>
|
specifications.
|
||||||
<li>The file might be a special "XFA" or dynamic form-based PDF, which some of our tools cannot
|
</li>
|
||||||
process.</li>
|
<li>
|
||||||
|
The file could be encrypted with a password you don't have.
|
||||||
|
Please use our Decrypt tool first.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The file might be a special "XFA" or dynamic form-based PDF,
|
||||||
|
which some of our tools cannot process.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">Do you track my activity on BentoPDF?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
Do you track my activity on BentoPDF?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>We care about your privacy. BentoPDF does not track personal information. We use <a
|
<p>
|
||||||
href="https://simpleanalytics.com" class="text-indigo-400 hover:underline" target="_blank"
|
We care about your privacy. BentoPDF does not track personal
|
||||||
rel="noopener noreferrer">Simple Analytics</a> solely to see anonymous visit counts. This
|
information. We use
|
||||||
means we can know how many users visit our site, but <strong>we never know who you are</strong>.
|
<a
|
||||||
Simple Analytics is fully GDPR-compliant and respects your privacy.</p>
|
href="https://simpleanalytics.com"
|
||||||
|
class="text-indigo-400 hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Simple Analytics</a
|
||||||
|
>
|
||||||
|
solely to see anonymous visit counts. This means we can know how
|
||||||
|
many users visit our site, but
|
||||||
|
<strong>we never know who you are</strong>. Simple Analytics is
|
||||||
|
fully GDPR-compliant and respects your privacy.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="bg-gray-800 border border-gray-700 rounded-lg p-5 group">
|
<details
|
||||||
|
class="bg-gray-800 border border-gray-700 rounded-lg p-5 group"
|
||||||
|
>
|
||||||
<summary class="flex items-center justify-between cursor-pointer">
|
<summary class="flex items-center justify-between cursor-pointer">
|
||||||
<h3 class="font-semibold text-white text-lg">What technology does BentoPDF use?</h3>
|
<h3 class="font-semibold text-white text-lg">
|
||||||
<i data-lucide="plus" class="w-6 h-6 text-indigo-400 icon flex-shrink-0"></i>
|
What technology does BentoPDF use?
|
||||||
|
</h3>
|
||||||
|
<i
|
||||||
|
data-lucide="plus"
|
||||||
|
class="w-6 h-6 text-indigo-400 icon flex-shrink-0"
|
||||||
|
></i>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="mt-4 text-gray-400">
|
<div class="mt-4 text-gray-400">
|
||||||
<p>BentoPDF is built on the power of modern web technologies. We primarily use JavaScript libraries
|
<p>
|
||||||
like <strong>PDF-lib.js</strong> and <strong>PDFKit.js</strong> that are compiled to run
|
BentoPDF is built on the power of modern web technologies. We
|
||||||
efficiently in your browser via WebAssembly. This allows for powerful, server-like processing
|
primarily use JavaScript libraries like
|
||||||
without your data ever leaving your machine.</p>
|
<strong>PDF-lib.js</strong> and <strong>PDFKit.js</strong> that
|
||||||
|
are compiled to run efficiently in your browser via WebAssembly.
|
||||||
|
This allows for powerful, server-like processing without your data
|
||||||
|
ever leaving your machine.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,56 +291,98 @@
|
|||||||
<div class="section-divider mt-16"></div>
|
<div class="section-divider mt-16"></div>
|
||||||
|
|
||||||
<section id="contact-section" class="text-center py-16">
|
<section id="contact-section" class="text-center py-16">
|
||||||
<h2 class="text-3xl font-bold text-white mb-4">Still have questions?</h2>
|
<h2 class="text-3xl font-bold text-white mb-4">
|
||||||
|
Still have questions?
|
||||||
|
</h2>
|
||||||
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
<p class="text-lg text-gray-400 mb-8 max-w-2xl mx-auto">
|
||||||
If you can't find the answer you're looking for, feel free to reach out to our support team. We're
|
If you can't find the answer you're looking for, feel free to reach
|
||||||
always Bento to help.
|
out to our support team. We're always Bento to help.
|
||||||
</p>
|
</p>
|
||||||
<a href="contact.html"
|
<a
|
||||||
class="inline-block px-8 py-3 rounded-full bg-gray-700 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:bg-gray-600 transition-colors duration-200">
|
href="contact.html"
|
||||||
|
class="inline-block px-8 py-3 rounded-full bg-gray-700 text-white font-semibold focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 hover:bg-gray-600 transition-colors duration-200"
|
||||||
|
>
|
||||||
Contact Us
|
Contact Us
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
|
>
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img src="images/favicon.svg" alt="Bento PDF Logo" class="h-10 w-10 mr-3">
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
|
/>
|
||||||
<span class="text-xl font-bold text-white">Bento PDF</span>
|
<span class="text-xl font-bold text-white">Bento PDF</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">© 2025 Bento PDF. All rights reserved.</p>
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 Bento PDF. All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="about.html" class="hover:text-indigo-400">About Us</a></li>
|
<li>
|
||||||
|
<a href="about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||||
<li><a href="contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
<li>
|
||||||
|
<a href="contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="./terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
<li>
|
||||||
<li><a href="./privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
<a href="./terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="./privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400"><i data-lucide="instagram"></i></a>
|
<a
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400"><i data-lucide="linkedin"></i></a>
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
><i data-lucide="instagram"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
href="https://www.linkedin.com/company/bentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
><i data-lucide="linkedin"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
href="https://x.com/BentoPDF"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,9 +391,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
lucide.createIcons();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+572
-240
File diff suppressed because it is too large.
Load diff
+3
-1
@@ -1,3 +1,5 @@
|
|||||||
|
pid /etc/nginx/tmp/nginx.pid;
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
}
|
}
|
||||||
@@ -15,7 +17,7 @@ http {
|
|||||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 8080;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|||||||
Generated
+312
-221
@@ -1,15 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"version": "0.0.0",
|
"version": "1.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"version": "0.0.0",
|
"version": "1.2.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||||
|
"@fontsource/dancing-script": "^5.2.8",
|
||||||
|
"@fontsource/dm-sans": "^5.2.8",
|
||||||
|
"@fontsource/great-vibes": "^5.2.8",
|
||||||
|
"@fontsource/kalam": "^5.2.8",
|
||||||
|
"@fontsource/lato": "^5.2.7",
|
||||||
|
"@fontsource/merriweather": "^5.2.11",
|
||||||
|
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.15",
|
||||||
"blob-stream": "^0.1.3",
|
"blob-stream": "^0.1.3",
|
||||||
"cropperjs": "^1.6.1",
|
"cropperjs": "^1.6.1",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
@@ -17,7 +25,7 @@
|
|||||||
"javascript-obfuscator": "^4.1.1",
|
"javascript-obfuscator": "^4.1.1",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.3",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide": "^0.545.0",
|
"lucide": "^0.546.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^5.4.296",
|
"pdfjs-dist": "^5.4.296",
|
||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
@@ -31,16 +39,17 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@types/blob-stream": "^0.1.33",
|
"@types/blob-stream": "^0.1.33",
|
||||||
"@types/html2canvas": "^0.5.35",
|
"@types/html2canvas": "^1.0.0",
|
||||||
"@types/pdfkit": "^0.17.3",
|
"@types/pdfkit": "^0.17.3",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@types/utif": "^3.0.6",
|
"@types/utif": "^3.0.6",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"ts-migrate": "^0.1.35",
|
"ts-migrate": "^0.1.35",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.11",
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
}
|
}
|
||||||
@@ -84,9 +93,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@asamuzakjp/dom-selector": {
|
"node_modules/@asamuzakjp/dom-selector": {
|
||||||
"version": "6.6.2",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.2.tgz",
|
||||||
"integrity": "sha512-+AG0jN9HTwfDLBhjhX1FKi6zlIAc/YGgEHlN/OMaHD1pOPFsC5CpYQpLkPX0aFjyaVmoq9330cQDCU4qnSL1qA==",
|
"integrity": "sha512-ccKogJI+0aiDhOahdjANIc9SDixSud1gbwdVrhn7kMopAtLXqsz9MKmQQtIl6Y5aC2IYq+j4dz/oedL2AVMmVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2574,6 +2583,69 @@
|
|||||||
"node": "^10.12.0 || >=12.0.0"
|
"node": "^10.12.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource/cedarville-cursive": {
|
||||||
|
"version": "5.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/cedarville-cursive/-/cedarville-cursive-5.2.7.tgz",
|
||||||
|
"integrity": "sha512-G1Wcc8U9MsFV7t4vmxSaWEDVcJj86qbkuS/V27S5UngxkX6CoTpEdvDeGUIKBlF8FoHCDOQxSNZCFuCafRmvUg==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/dancing-script": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/dancing-script/-/dancing-script-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-zHVVgIQ5/rAIM0VVp1QMsqvJm9INHO5/C82E9rHT6NlabjaPTFYn8wl9lT4vPWizfB0TublGQM1999kfdA+XFA==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/dm-sans": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/dm-sans/-/dm-sans-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-tlovG42m9ESG28WiHpLq3F5umAlm64rv0RkqTbYowRn70e9OlRr5a3yTJhrhrY+k5lftR/OFJjPzOLQzk8EfCA==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/great-vibes": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/great-vibes/-/great-vibes-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-/PATz436NGixjNaigierUpv50wfNv2UhAAijcCPkn4ynvl41x66TCLbXVSVfp4pth9gjWnGqgoj0lVIhHapAuw==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/kalam": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/kalam/-/kalam-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-9WDYPX1llS28xRjrqAoz3IOLeaBhCtL3HCtNanYnF1fthFPmSD5aESL6s7C1SJxAlxq/qzTlpIpw4S1W9+q3OA==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/lato": {
|
||||||
|
"version": "5.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/lato/-/lato-5.2.7.tgz",
|
||||||
|
"integrity": "sha512-k5mum1ADbDW5cTw1Ett1eQVWeoZ6gq0ct6SFBibEhB4LRxhniChJZTBgd6ph5yBxLkN1fcnsnmicBNA4S/3nbw==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/merriweather": {
|
||||||
|
"version": "5.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/merriweather/-/merriweather-5.2.11.tgz",
|
||||||
|
"integrity": "sha512-ZiIMeUh5iT8d73o6xlSF8GKgjV5pgiFrufYc5jZTVAfExtWKqM2vQHnsqXSFMv4ELhAcjt6Vf+5T3oVGXhAizQ==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
|
||||||
@@ -2701,18 +2773,6 @@
|
|||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@isaacs/fs-minipass": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"minipass": "^7.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@istanbuljs/schema": {
|
"node_modules/@istanbuljs/schema": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||||
@@ -2990,6 +3050,12 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@neslinesli93/qpdf-wasm": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@neslinesli93/qpdf-wasm/-/qpdf-wasm-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-ObyTmabopTMwN6AGwrEkmDbdmM5wnwlRdn9jnJYQ2KbBUPdPDcfNf1QkcWFOVD+qIgSqILz9nK51e22T+x+gkQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -3412,52 +3478,47 @@
|
|||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.15.tgz",
|
||||||
"integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==",
|
"integrity": "sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
"enhanced-resolve": "^5.18.3",
|
"enhanced-resolve": "^5.18.3",
|
||||||
"jiti": "^2.6.0",
|
"jiti": "^2.6.0",
|
||||||
"lightningcss": "1.30.1",
|
"lightningcss": "1.30.2",
|
||||||
"magic-string": "^0.30.19",
|
"magic-string": "^0.30.19",
|
||||||
"source-map-js": "^1.2.1",
|
"source-map-js": "^1.2.1",
|
||||||
"tailwindcss": "4.1.14"
|
"tailwindcss": "4.1.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide": {
|
"node_modules/@tailwindcss/oxide": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.15.tgz",
|
||||||
"integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==",
|
"integrity": "sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==",
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"detect-libc": "^2.0.4",
|
|
||||||
"tar": "^7.5.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@tailwindcss/oxide-android-arm64": "4.1.14",
|
"@tailwindcss/oxide-android-arm64": "4.1.15",
|
||||||
"@tailwindcss/oxide-darwin-arm64": "4.1.14",
|
"@tailwindcss/oxide-darwin-arm64": "4.1.15",
|
||||||
"@tailwindcss/oxide-darwin-x64": "4.1.14",
|
"@tailwindcss/oxide-darwin-x64": "4.1.15",
|
||||||
"@tailwindcss/oxide-freebsd-x64": "4.1.14",
|
"@tailwindcss/oxide-freebsd-x64": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14",
|
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.14",
|
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": "4.1.14",
|
"@tailwindcss/oxide-linux-arm64-musl": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": "4.1.14",
|
"@tailwindcss/oxide-linux-x64-gnu": "4.1.15",
|
||||||
"@tailwindcss/oxide-linux-x64-musl": "4.1.14",
|
"@tailwindcss/oxide-linux-x64-musl": "4.1.15",
|
||||||
"@tailwindcss/oxide-wasm32-wasi": "4.1.14",
|
"@tailwindcss/oxide-wasm32-wasi": "4.1.15",
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.14",
|
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.15",
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": "4.1.14"
|
"@tailwindcss/oxide-win32-x64-msvc": "4.1.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-android-arm64": {
|
"node_modules/@tailwindcss/oxide-android-arm64": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.15.tgz",
|
||||||
"integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==",
|
"integrity": "sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3471,9 +3532,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
"node_modules/@tailwindcss/oxide-darwin-arm64": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.15.tgz",
|
||||||
"integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==",
|
"integrity": "sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3487,9 +3548,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
"node_modules/@tailwindcss/oxide-darwin-x64": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.15.tgz",
|
||||||
"integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==",
|
"integrity": "sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3503,9 +3564,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
"node_modules/@tailwindcss/oxide-freebsd-x64": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.15.tgz",
|
||||||
"integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==",
|
"integrity": "sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3519,9 +3580,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.15.tgz",
|
||||||
"integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==",
|
"integrity": "sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -3535,9 +3596,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.15.tgz",
|
||||||
"integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==",
|
"integrity": "sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3551,9 +3612,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.15.tgz",
|
||||||
"integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==",
|
"integrity": "sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3567,9 +3628,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.15.tgz",
|
||||||
"integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==",
|
"integrity": "sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3583,9 +3644,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.15.tgz",
|
||||||
"integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==",
|
"integrity": "sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3599,9 +3660,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.15.tgz",
|
||||||
"integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==",
|
"integrity": "sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==",
|
||||||
"bundleDependencies": [
|
"bundleDependencies": [
|
||||||
"@napi-rs/wasm-runtime",
|
"@napi-rs/wasm-runtime",
|
||||||
"@emnapi/core",
|
"@emnapi/core",
|
||||||
@@ -3619,7 +3680,7 @@
|
|||||||
"@emnapi/core": "^1.5.0",
|
"@emnapi/core": "^1.5.0",
|
||||||
"@emnapi/runtime": "^1.5.0",
|
"@emnapi/runtime": "^1.5.0",
|
||||||
"@emnapi/wasi-threads": "^1.1.0",
|
"@emnapi/wasi-threads": "^1.1.0",
|
||||||
"@napi-rs/wasm-runtime": "^1.0.5",
|
"@napi-rs/wasm-runtime": "^1.0.7",
|
||||||
"@tybys/wasm-util": "^0.10.1",
|
"@tybys/wasm-util": "^0.10.1",
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
},
|
},
|
||||||
@@ -3627,10 +3688,64 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.1.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/core": "^1.5.0",
|
||||||
|
"@emnapi/runtime": "^1.5.0",
|
||||||
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.15.tgz",
|
||||||
"integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==",
|
"integrity": "sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -3644,9 +3759,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.15.tgz",
|
||||||
"integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==",
|
"integrity": "sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3660,14 +3775,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/vite": {
|
"node_modules/@tailwindcss/vite": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.15.tgz",
|
||||||
"integrity": "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==",
|
"integrity": "sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/node": "4.1.14",
|
"@tailwindcss/node": "4.1.15",
|
||||||
"@tailwindcss/oxide": "4.1.14",
|
"@tailwindcss/oxide": "4.1.15",
|
||||||
"tailwindcss": "4.1.14"
|
"tailwindcss": "4.1.15"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
@@ -3793,23 +3908,14 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/html2canvas": {
|
"node_modules/@types/html2canvas": {
|
||||||
"version": "0.5.35",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-0.5.35.tgz",
|
"resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz",
|
||||||
"integrity": "sha512-1A2dtWZbOIZ+rUK8jmAx1We/EiNV+5vScpphU3AF14Vby6COIazi/9StosrvlVCqlQegRhsEgZf7QYOuWbwuuA==",
|
"integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==",
|
||||||
|
"deprecated": "This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed.",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/jquery": "*"
|
"html2canvas": "*"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/jquery": {
|
|
||||||
"version": "3.5.33",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.33.tgz",
|
|
||||||
"integrity": "sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/sizzle": "*"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/minimatch": {
|
"node_modules/@types/minimatch": {
|
||||||
@@ -3851,13 +3957,6 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/sizzle": {
|
|
||||||
"version": "2.3.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz",
|
|
||||||
"integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/sortablejs": {
|
"node_modules/@types/sortablejs": {
|
||||||
"version": "1.15.8",
|
"version": "1.15.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz",
|
||||||
@@ -5038,15 +5137,6 @@
|
|||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chownr": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cipher-base": {
|
"node_modules/cipher-base": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
|
||||||
@@ -8080,21 +8170,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsdom": {
|
"node_modules/jsdom": {
|
||||||
"version": "27.0.0",
|
"version": "27.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz",
|
||||||
"integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==",
|
"integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asamuzakjp/dom-selector": "^6.5.4",
|
"@asamuzakjp/dom-selector": "^6.7.2",
|
||||||
"cssstyle": "^5.3.0",
|
"cssstyle": "^5.3.1",
|
||||||
"data-urls": "^6.0.0",
|
"data-urls": "^6.0.0",
|
||||||
"decimal.js": "^10.5.0",
|
"decimal.js": "^10.6.0",
|
||||||
"html-encoding-sniffer": "^4.0.0",
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
"http-proxy-agent": "^7.0.2",
|
"http-proxy-agent": "^7.0.2",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
"is-potential-custom-element-name": "^1.0.1",
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
"parse5": "^7.3.0",
|
"parse5": "^8.0.0",
|
||||||
"rrweb-cssom": "^0.8.0",
|
"rrweb-cssom": "^0.8.0",
|
||||||
"saxes": "^6.0.0",
|
"saxes": "^6.0.0",
|
||||||
"symbol-tree": "^3.2.4",
|
"symbol-tree": "^3.2.4",
|
||||||
@@ -8103,8 +8193,8 @@
|
|||||||
"webidl-conversions": "^8.0.0",
|
"webidl-conversions": "^8.0.0",
|
||||||
"whatwg-encoding": "^3.1.1",
|
"whatwg-encoding": "^3.1.1",
|
||||||
"whatwg-mimetype": "^4.0.0",
|
"whatwg-mimetype": "^4.0.0",
|
||||||
"whatwg-url": "^15.0.0",
|
"whatwg-url": "^15.1.0",
|
||||||
"ws": "^8.18.2",
|
"ws": "^8.18.3",
|
||||||
"xml-name-validator": "^5.0.0"
|
"xml-name-validator": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -8298,9 +8388,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
@@ -8313,22 +8403,43 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"lightningcss-darwin-arm64": "1.30.1",
|
"lightningcss-android-arm64": "1.30.2",
|
||||||
"lightningcss-darwin-x64": "1.30.1",
|
"lightningcss-darwin-arm64": "1.30.2",
|
||||||
"lightningcss-freebsd-x64": "1.30.1",
|
"lightningcss-darwin-x64": "1.30.2",
|
||||||
"lightningcss-linux-arm-gnueabihf": "1.30.1",
|
"lightningcss-freebsd-x64": "1.30.2",
|
||||||
"lightningcss-linux-arm64-gnu": "1.30.1",
|
"lightningcss-linux-arm-gnueabihf": "1.30.2",
|
||||||
"lightningcss-linux-arm64-musl": "1.30.1",
|
"lightningcss-linux-arm64-gnu": "1.30.2",
|
||||||
"lightningcss-linux-x64-gnu": "1.30.1",
|
"lightningcss-linux-arm64-musl": "1.30.2",
|
||||||
"lightningcss-linux-x64-musl": "1.30.1",
|
"lightningcss-linux-x64-gnu": "1.30.2",
|
||||||
"lightningcss-win32-arm64-msvc": "1.30.1",
|
"lightningcss-linux-x64-musl": "1.30.2",
|
||||||
"lightningcss-win32-x64-msvc": "1.30.1"
|
"lightningcss-win32-arm64-msvc": "1.30.2",
|
||||||
|
"lightningcss-win32-x64-msvc": "1.30.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lightningcss-android-arm64": {
|
||||||
|
"version": "1.30.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
|
||||||
|
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-arm64": {
|
"node_modules/lightningcss-darwin-arm64": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
|
||||||
"integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
|
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8346,9 +8457,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-darwin-x64": {
|
"node_modules/lightningcss-darwin-x64": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
|
||||||
"integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
|
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8366,9 +8477,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-freebsd-x64": {
|
"node_modules/lightningcss-freebsd-x64": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
|
||||||
"integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
|
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8386,9 +8497,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
|
||||||
"integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
|
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -8406,9 +8517,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
|
||||||
"integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
|
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8426,9 +8537,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-arm64-musl": {
|
"node_modules/lightningcss-linux-arm64-musl": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
|
||||||
"integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
|
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8446,9 +8557,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-gnu": {
|
"node_modules/lightningcss-linux-x64-gnu": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
|
||||||
"integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
|
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8466,9 +8577,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-linux-x64-musl": {
|
"node_modules/lightningcss-linux-x64-musl": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
|
||||||
"integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
|
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8486,9 +8597,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
|
||||||
"integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
|
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -8506,9 +8617,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lightningcss-win32-x64-msvc": {
|
"node_modules/lightningcss-win32-x64-msvc": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
|
||||||
"integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
|
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -8622,9 +8733,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/lucide": {
|
"node_modules/lucide": {
|
||||||
"version": "0.545.0",
|
"version": "0.546.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide/-/lucide-0.545.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide/-/lucide-0.546.0.tgz",
|
||||||
"integrity": "sha512-mrBH0upkb1TH8ZLkf0XERQAKMdTJ1C+Offr7eSvanmdQ7JrV8jkrFw5jgRAS8fygiZgG4X9mfEg/BXCL+mNMRw==",
|
"integrity": "sha512-YJES3MM/naQS4wJ0JLzTY3anooqWw5iTsPCffHbSMncdxJT2C5tmkCDAwIHMHG8TMtaQcu40KREMPss2qH/+yA==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/lz-string": {
|
"node_modules/lz-string": {
|
||||||
@@ -8847,23 +8958,12 @@
|
|||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minizlib": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"minipass": "^7.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mixin-deep": {
|
"node_modules/mixin-deep": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
|
||||||
@@ -9462,9 +9562,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/parse5": {
|
"node_modules/parse5": {
|
||||||
"version": "7.3.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
||||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -9763,6 +9863,22 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pretty-format": {
|
"node_modules/pretty-format": {
|
||||||
"version": "27.5.1",
|
"version": "27.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
@@ -11239,9 +11355,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.14",
|
"version": "4.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
|
||||||
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
|
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
@@ -11257,22 +11373,6 @@
|
|||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
|
||||||
"version": "7.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz",
|
|
||||||
"integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@isaacs/fs-minipass": "^4.0.0",
|
|
||||||
"chownr": "^3.0.0",
|
|
||||||
"minipass": "^7.1.2",
|
|
||||||
"minizlib": "^3.1.0",
|
|
||||||
"yallist": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/temp": {
|
"node_modules/temp": {
|
||||||
"version": "0.8.4",
|
"version": "0.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz",
|
||||||
@@ -12232,9 +12332,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.1.9",
|
"version": "7.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
|
||||||
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
|
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
@@ -12747,15 +12847,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/yallist": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
|
||||||
"license": "BlueOak-1.0.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "15.4.1",
|
"version": "15.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
|||||||
+21
-7
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bento-pdf",
|
"name": "bento-pdf",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.3.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -14,26 +14,40 @@
|
|||||||
"test:run": "vitest run",
|
"test:run": "vitest run",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:watch": "vitest watch",
|
"test:watch": "vitest watch",
|
||||||
"build:docker": "vite build"
|
"build:docker": "vite build",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"release": "node scripts/release.js patch",
|
||||||
|
"release:minor": "node scripts/release.js minor",
|
||||||
|
"release:major": "node scripts/release.js major",
|
||||||
|
"serve:simple": "SIMPLE_MODE=true npm run build && npx serve dist -p 3000"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@types/blob-stream": "^0.1.33",
|
"@types/blob-stream": "^0.1.33",
|
||||||
"@types/html2canvas": "^0.5.35",
|
"@types/html2canvas": "^1.0.0",
|
||||||
"@types/pdfkit": "^0.17.3",
|
"@types/pdfkit": "^0.17.3",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@types/utif": "^3.0.6",
|
"@types/utif": "^3.0.6",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/coverage-v8": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.1",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"ts-migrate": "^0.1.35",
|
"ts-migrate": "^0.1.35",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.11",
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
"vite-plugin-node-polyfills": "^0.24.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@fontsource/cedarville-cursive": "^5.2.7",
|
||||||
|
"@fontsource/dancing-script": "^5.2.8",
|
||||||
|
"@fontsource/dm-sans": "^5.2.8",
|
||||||
|
"@fontsource/great-vibes": "^5.2.8",
|
||||||
|
"@fontsource/kalam": "^5.2.8",
|
||||||
|
"@fontsource/lato": "^5.2.7",
|
||||||
|
"@fontsource/merriweather": "^5.2.11",
|
||||||
|
"@neslinesli93/qpdf-wasm": "^0.3.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.15",
|
||||||
"blob-stream": "^0.1.3",
|
"blob-stream": "^0.1.3",
|
||||||
"cropperjs": "^1.6.1",
|
"cropperjs": "^1.6.1",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
@@ -41,7 +55,7 @@
|
|||||||
"javascript-obfuscator": "^4.1.1",
|
"javascript-obfuscator": "^4.1.1",
|
||||||
"jspdf": "^3.0.3",
|
"jspdf": "^3.0.3",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide": "^0.545.0",
|
"lucide": "^0.546.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pdfjs-dist": "^5.4.296",
|
"pdfjs-dist": "^5.4.296",
|
||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
|
|||||||
+233
-78
@@ -1,25 +1,31 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Privacy Policy - BentoPDF</title>
|
<title>Privacy Policy - BentoPDF</title>
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
<link href="/src/css/styles.css" rel="stylesheet" />
|
||||||
<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>
|
</head>
|
||||||
|
|
||||||
<body class="antialiased bg-gray-900 text-gray-300">
|
<body class="antialiased bg-gray-900 text-gray-300">
|
||||||
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="flex justify-between items-center h-16">
|
<div class="flex justify-between items-center h-16">
|
||||||
<a href="index.html" class="flex-shrink-0 flex items-center cursor-pointer">
|
<div
|
||||||
<img src="images/favicon.svg" alt="BentoPDF Logo" class="h-8 w-8 mr-2">
|
class="flex-shrink-0 flex items-center cursor-pointer"
|
||||||
<span class="text-white font-bold text-xl">BentoPDF</span>
|
id="home-logo"
|
||||||
</a>
|
>
|
||||||
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="Bento PDF Logo"
|
||||||
|
class="h-8 w-8"
|
||||||
|
/>
|
||||||
|
<span class="text-white font-bold text-xl ml-2">
|
||||||
|
<a href="index.html">BentoPDF</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Navigation -->
|
||||||
<div class="hidden md:flex items-center space-x-8">
|
<div class="hidden md:flex items-center space-x-8">
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<a href="index.html" class="nav-link">Home</a>
|
||||||
<a href="./about.html" class="nav-link">About</a>
|
<a href="./about.html" class="nav-link">About</a>
|
||||||
@@ -27,130 +33,280 @@
|
|||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden flex items-center gap-4">
|
<!-- Mobile Hamburger Button -->
|
||||||
<a href="index.html" class="nav-link">Home</a>
|
<div class="md:hidden flex items-center">
|
||||||
<a href="index.html#tools-header" class="nav-link">All Tools</a>
|
<button
|
||||||
|
id="mobile-menu-button"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 transition-colors"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<!-- Hamburger Icon -->
|
||||||
|
<svg
|
||||||
|
id="menu-icon"
|
||||||
|
class="block h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- Close Icon -->
|
||||||
|
<svg
|
||||||
|
id="close-icon"
|
||||||
|
class="hidden h-6 w-6"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Dropdown -->
|
||||||
|
<div
|
||||||
|
id="mobile-menu"
|
||||||
|
class="hidden md:hidden bg-gray-800 border-t border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1 text-center">
|
||||||
|
<a href="index.html" class="mobile-nav-link">Home</a>
|
||||||
|
<a href="./about.html" class="mobile-nav-link">About</a>
|
||||||
|
<a href="./contact.html" class="mobile-nav-link">Contact</a>
|
||||||
|
<a href="index.html#tools-header" class="mobile-nav-link"
|
||||||
|
>All Tools</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="app" class="container mx-auto p-4 md:p-8">
|
<div id="app" class="container mx-auto p-4 md:p-8">
|
||||||
<section class="max-w-4xl mx-auto py-12">
|
<section class="max-w-4xl mx-auto py-12">
|
||||||
<h1 class="text-4xl md:text-5xl font-bold text-center text-white mb-4">Privacy Policy</h1>
|
<h1 class="text-4xl md:text-5xl font-bold text-center text-white mb-4">
|
||||||
<p class="text-center text-gray-500">Last Updated: September 14, 2025</p>
|
Privacy Policy
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-500">
|
||||||
|
Last Updated: September 14, 2025
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="legal-content mt-12">
|
<div class="legal-content mt-12">
|
||||||
<h2>1. Our Commitment to Privacy</h2>
|
<h2>1. Our Commitment to Privacy</h2>
|
||||||
<p>BentoPDF ("we", "us", "our") is fundamentally a privacy-focused service. This Privacy Policy outlines
|
<p>
|
||||||
our unwavering commitment to protecting your privacy. Our core principle is simple: <strong>your
|
BentoPDF ("we", "us", "our") is fundamentally a privacy-focused
|
||||||
files are your files</strong>. We do not and cannot view, access, store, or share your
|
service. This Privacy Policy outlines our unwavering commitment to
|
||||||
documents. All PDF processing occurs entirely on your own computer, within your web browser
|
protecting your privacy. Our core principle is simple:
|
||||||
(client-side).</p>
|
<strong>your files are your files</strong>. We do not and cannot
|
||||||
|
view, access, store, or share your documents. All PDF processing
|
||||||
|
occurs entirely on your own computer, within your web browser
|
||||||
|
(client-side).
|
||||||
|
</p>
|
||||||
|
|
||||||
<h3>1.1 The Client-Side Principle</h3>
|
<h3>1.1 The Client-Side Principle</h3>
|
||||||
<p>Unlike other online PDF services, BentoPDF does not upload your files to a server for processing. The
|
<p>
|
||||||
tools you use are powered by JavaScript and WebAssembly libraries that run directly on your device.
|
Unlike other online PDF services, BentoPDF does not upload your
|
||||||
This means your data never leaves your computer, providing you with the highest level of privacy and
|
files to a server for processing. The tools you use are powered by
|
||||||
security.</p>
|
JavaScript and WebAssembly libraries that run directly on your
|
||||||
|
device. This means your data never leaves your computer, providing
|
||||||
|
you with the highest level of privacy and security.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>2. Information We Do Not Collect</h2>
|
<h2>2. Information We Do Not Collect</h2>
|
||||||
<p>Because of our client-side architecture, we are technically incapable of collecting the following
|
<p>
|
||||||
information:</p>
|
Because of our client-side architecture, we are technically
|
||||||
|
incapable of collecting the following information:
|
||||||
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>The content of your PDF files or any other documents you use with our tools.</li>
|
<li>
|
||||||
|
The content of your PDF files or any other documents you use with
|
||||||
|
our tools.
|
||||||
|
</li>
|
||||||
<li>Any personal data contained within your documents.</li>
|
<li>Any personal data contained within your documents.</li>
|
||||||
<li>Filenames of your documents.</li>
|
<li>Filenames of your documents.</li>
|
||||||
<li>Any derived information or metadata from your files, beyond what is necessary for the tool to
|
<li>
|
||||||
function during your active session (and this is immediately discarded).</li>
|
Any derived information or metadata from your files, beyond what
|
||||||
|
is necessary for the tool to function during your active session
|
||||||
|
(and this is immediately discarded).
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>3. Information We May Collect (Non-Personal Data)</h2>
|
<h2>3. Information We May Collect (Non-Personal Data)</h2>
|
||||||
<p>To improve our website and services, we may collect anonymous, non-personally identifiable
|
<p>
|
||||||
information. This type of data helps us understand how users interact with our site, which tools are
|
To improve our website and services, we may collect anonymous,
|
||||||
most popular, and how we can improve the user experience. This includes:</p>
|
non-personally identifiable information. This type of data helps us
|
||||||
|
understand how users interact with our site, which tools are most
|
||||||
|
popular, and how we can improve the user experience. This includes:
|
||||||
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>Usage Analytics:</strong> Anonymized data such as which tools are used, how often they
|
<li>
|
||||||
are used, and which features are accessed. This is aggregated and cannot be tied back to an
|
<strong>Usage Analytics:</strong> Anonymized data such as which
|
||||||
individual user or document.</li>
|
tools are used, how often they are used, and which features are
|
||||||
<li><strong>Performance Data:</strong> Anonymized error reports or performance metrics to help us
|
accessed. This is aggregated and cannot be tied back to an
|
||||||
identify and fix bugs. This data contains no personal information or file content.</li>
|
individual user or document.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Performance Data:</strong> Anonymized error reports or
|
||||||
|
performance metrics to help us identify and fix bugs. This data
|
||||||
|
contains no personal information or file content.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>We use privacy-respecting analytics platforms for this purpose. Specifically, we use
|
<p>
|
||||||
<a href="https://simpleanalytics.com" target="_blank" rel="noopener noreferrer"
|
We use privacy-respecting analytics platforms for this purpose.
|
||||||
class="text-indigo-400 hover:underline">Simple Analytics</a>
|
Specifically, we use
|
||||||
to track anonymous visit counts. This means we can see how many users visit our site, but
|
<a
|
||||||
<strong>we never collect personal information or identify individual users</strong>.
|
href="https://simpleanalytics.com"
|
||||||
Simple Analytics is fully GDPR-compliant and respects user privacy. We do not use tracking cookies
|
target="_blank"
|
||||||
for advertising or cross-site profiling.
|
rel="noopener noreferrer"
|
||||||
|
class="text-indigo-400 hover:underline"
|
||||||
|
>Simple Analytics</a
|
||||||
|
>
|
||||||
|
to track anonymous visit counts. This means we can see how many
|
||||||
|
users visit our site, but
|
||||||
|
<strong
|
||||||
|
>we never collect personal information or identify individual
|
||||||
|
users</strong
|
||||||
|
>. Simple Analytics is fully GDPR-compliant and respects user
|
||||||
|
privacy. We do not use tracking cookies for advertising or
|
||||||
|
cross-site profiling.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>4. Third-Party Libraries</h2>
|
<h2>4. Third-Party Libraries</h2>
|
||||||
<p>BentoPDF is built using powerful, open-source libraries like PDF-lib.js and PDF.js. These libraries
|
<p>
|
||||||
are trusted by developers worldwide and operate under the same client-side principle. While we have
|
BentoPDF is built using powerful, open-source libraries like
|
||||||
vetted these libraries, we encourage you to review their respective privacy policies for your own
|
PDF-lib.js and PDF.js. These libraries are trusted by developers
|
||||||
peace of mind.</p>
|
worldwide and operate under the same client-side principle. While we
|
||||||
|
have vetted these libraries, we encourage you to review their
|
||||||
|
respective privacy policies for your own peace of mind.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>5. Security</h2>
|
<h2>5. Security</h2>
|
||||||
<p>Since your files are never transmitted over the internet to our servers, you are protected from
|
<p>
|
||||||
potential data breaches during transit or storage on a server. The security of your documents is in
|
Since your files are never transmitted over the internet to our
|
||||||
your hands and protected by the security of your own computer and web browser.</p>
|
servers, you are protected from potential data breaches during
|
||||||
|
transit or storage on a server. The security of your documents is in
|
||||||
|
your hands and protected by the security of your own computer and
|
||||||
|
web browser.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>6. Children's Privacy</h2>
|
<h2>6. Children's Privacy</h2>
|
||||||
<p>Our services are not directed at individuals under the age of 13. We do not knowingly collect any
|
<p>
|
||||||
personal information from children. If you believe a child has provided us with personal
|
Our services are not directed at individuals under the age of 13. We
|
||||||
information, please contact us, and we will take steps to delete such information.</p>
|
do not knowingly collect any personal information from children. If
|
||||||
|
you believe a child has provided us with personal information,
|
||||||
|
please contact us, and we will take steps to delete such
|
||||||
|
information.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>7. Changes to This Privacy Policy</h2>
|
<h2>7. Changes to This Privacy Policy</h2>
|
||||||
<p>We may update this Privacy Policy from time to time. We will notify you of any changes by posting the
|
<p>
|
||||||
new policy on this page and updating the "Last Updated" date at the top. You are advised to review
|
We may update this Privacy Policy from time to time. We will notify
|
||||||
this Privacy Policy periodically for any changes.</p>
|
you of any changes by posting the new policy on this page and
|
||||||
|
updating the "Last Updated" date at the top. You are advised to
|
||||||
|
review this Privacy Policy periodically for any changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2>8. Contact Us</h2>
|
<h2>8. Contact Us</h2>
|
||||||
<p>If you have any questions about this Privacy Policy, please contact us at <a
|
<p>
|
||||||
href="mailto:contact@bentopdf.com">contact@bentopdf.com</a>.</p>
|
If you have any questions about this Privacy Policy, please contact
|
||||||
|
us at
|
||||||
|
<a href="mailto:contact@bentopdf.com">contact@bentopdf.com</a>.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
<footer class="mt-16 border-t-2 border-gray-700 py-8">
|
||||||
<div class="container mx-auto px-4">
|
<div class="container mx-auto px-4">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left">
|
<div
|
||||||
|
class="grid grid-cols-1 md:grid-cols-4 gap-8 text-center md:text-left"
|
||||||
|
>
|
||||||
<div class="mb-8 md:mb-0">
|
<div class="mb-8 md:mb-0">
|
||||||
<div class="flex items-center justify-center md:justify-start mb-4">
|
<div class="flex items-center justify-center md:justify-start mb-4">
|
||||||
<img src="images/favicon.svg" alt="BentoPDF Logo" class="h-10 w-10 mr-3">
|
<img
|
||||||
|
src="images/favicon.svg"
|
||||||
|
alt="BentoPDF Logo"
|
||||||
|
class="h-10 w-10 mr-3"
|
||||||
|
/>
|
||||||
<span class="text-xl font-bold text-white">BentoPDF</span>
|
<span class="text-xl font-bold text-white">BentoPDF</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400 text-sm">© 2025 BentoPDF. All rights reserved.</p>
|
<p class="text-gray-400 text-sm">
|
||||||
|
© 2025 BentoPDF. All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Company</h3>
|
<h3 class="font-bold text-white mb-4">Company</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="about.html" class="hover:text-indigo-400">About Us</a></li>
|
<li>
|
||||||
|
<a href="about.html" class="hover:text-indigo-400">About Us</a>
|
||||||
|
</li>
|
||||||
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
<li><a href="faq.html" class="hover:text-indigo-400">FAQ</a></li>
|
||||||
<li><a href="contact.html" class="hover:text-indigo-400">Contact Us</a></li>
|
<li>
|
||||||
|
<a href="contact.html" class="hover:text-indigo-400"
|
||||||
|
>Contact Us</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Legal</h3>
|
<h3 class="font-bold text-white mb-4">Legal</h3>
|
||||||
<ul class="space-y-2 text-gray-400">
|
<ul class="space-y-2 text-gray-400">
|
||||||
<li><a href="terms.html" class="hover:text-indigo-400">Terms and Conditions</a></li>
|
<li>
|
||||||
<li><a href="privacy.html" class="hover:text-indigo-400">Privacy Policy</a></li>
|
<a href="terms.html" class="hover:text-indigo-400"
|
||||||
|
>Terms and Conditions</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="privacy.html" class="hover:text-indigo-400"
|
||||||
|
>Privacy Policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
<h3 class="font-bold text-white mb-4">Follow Us</h3>
|
||||||
<div class="flex justify-center md:justify-start space-x-4">
|
<div class="flex justify-center md:justify-start space-x-4">
|
||||||
<a href="https://www.instagram.com/thebentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.instagram.com/thebentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="instagram"></i>
|
<i data-lucide="instagram"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://www.linkedin.com/company/bentopdf/" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
|
href="https://www.linkedin.com/company/bentopdf/"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
<i data-lucide="linkedin"></i>
|
<i data-lucide="linkedin"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://x.com/BentoPDF" class="text-gray-400 hover:text-indigo-400">
|
<a
|
||||||
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
href="https://x.com/BentoPDF"
|
||||||
|
class="text-gray-400 hover:text-indigo-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<path
|
<path
|
||||||
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,9 +314,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<script>
|
<script type="module" src="src/js/utils/lucide-init.ts"></script>
|
||||||
lucide.createIcons();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
<script type="module" src="src/js/mobileMenu.ts"></script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
### Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
||||||
|
|
||||||
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
### [v1.0.0](https://github.com/alam00000/bentopdf/compare/v0.0.1...v1.0.0)
|
||||||
|
|
||||||
|
> 20 October 2025
|
||||||
|
|
||||||
|
- feat(ocr): add whitelist presets and improve UI for OCR tool [`0e7c535`](https://github.com/alam00000/bentopdf/commit/0e7c53560a23d3e26a18904aa47ca99d3f10d9a6)
|
||||||
|
- Release v1.0.0 [`6ad6a7d`](https://github.com/alam00000/bentopdf/commit/6ad6a7d7305eb77fe9ebdb55d93059bf92304a98)
|
||||||
|
|
||||||
|
#### v0.0.1
|
||||||
|
|
||||||
|
> 20 October 2025
|
||||||
|
|
||||||
|
- feat: add Simple Mode for internal organizational use [`#64`](https://github.com/alam00000/bentopdf/pull/64)
|
||||||
|
- Feature/reverse multi pdfs [`#58`](https://github.com/alam00000/bentopdf/pull/58)
|
||||||
|
- added description to Unraid XML [`#65`](https://github.com/alam00000/bentopdf/pull/65)
|
||||||
|
- Added hamburger Navbar [`#62`](https://github.com/alam00000/bentopdf/pull/62)
|
||||||
|
- feat(versioning): Added version management sysytem [`#52`](https://github.com/alam00000/bentopdf/pull/52)
|
||||||
|
- Fix#20 : fixed the tesseract issue [`#60`](https://github.com/alam00000/bentopdf/pull/60)
|
||||||
|
- feat: add search bar keyboard shortcut [`#49`](https://github.com/alam00000/bentopdf/pull/49)
|
||||||
|
- feat(pdf-tools): add attachments feature to embed files in PDFs [`#53`](https://github.com/alam00000/bentopdf/pull/53)
|
||||||
|
- feat(pdf-tools): add linearize pdf tool for web optimization [`#51`](https://github.com/alam00000/bentopdf/pull/51)
|
||||||
|
- feat: add support for multiple blank pages insertion [`#42`](https://github.com/alam00000/bentopdf/pull/42)
|
||||||
|
- Multiupload handle encrypted pdf [`#43`](https://github.com/alam00000/bentopdf/pull/43)
|
||||||
|
- feat:Setup Prettier for code formatting [`#30`](https://github.com/alam00000/bentopdf/pull/30)
|
||||||
|
- created png as unraid does not supprot svg [`#24`](https://github.com/alam00000/bentopdf/pull/24)
|
||||||
|
- adding unraid template [`#23`](https://github.com/alam00000/bentopdf/pull/23)
|
||||||
|
- feat(pdf-tools): add alternate merge tool for mixing pdf pages [`#21`](https://github.com/alam00000/bentopdf/pull/21)
|
||||||
|
- feat: add remove blank pages tool with sensitivity control [`#19`](https://github.com/alam00000/bentopdf/pull/19)
|
||||||
|
- Posterize [`#17`](https://github.com/alam00000/bentopdf/pull/17)
|
||||||
|
- feat: cicd for build and push to docker registry [`#15`](https://github.com/alam00000/bentopdf/pull/15)
|
||||||
|
- Fix(posterize): Resolve incorrect vertical tiling/cropping [`#10`](https://github.com/alam00000/bentopdf/pull/10)
|
||||||
|
- feat: add initial project setup with core PDF tools and utilities [`6712973`](https://github.com/alam00000/bentopdf/commit/671297320ec6ff93d05ad150836dd0a2b291c83c)
|
||||||
|
- feat(test): add comprehensive test suite and testing infrastructure [`5e72d7c`](https://github.com/alam00000/bentopdf/commit/5e72d7ca2debe62fc3e74606b967cdb8334ce827)
|
||||||
|
- refactor: fix type errors and improve type safety in PDF logic [`cc83d79`](https://github.com/alam00000/bentopdf/commit/cc83d7951f8748bb3b40223fc1ffd5d397faad72)
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* This file contains a modified version of cpdf.js from the CoherentPDF project.
|
||||||
|
* Original source: https://www.coherentpdf.com
|
||||||
|
* Portions © Coherent Graphics Ltd. (CoherentPDF project)
|
||||||
|
* Copyright © 2025 BentoPDF
|
||||||
|
*
|
||||||
|
* The original CoherentPDF code is licensed under the GNU Affero General Public License v3.0 (AGPLv3),
|
||||||
|
* and this modified version is distributed under the same license.
|
||||||
|
*/
|
||||||
|
!function(t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).coherentpdf=t()}(function(){return function n(i,a,f){function o(e,t){if(!a[e]){if(!i[e]){var r="function"==typeof require&&require;if(!t&&r)return r(e,!0);if(u)return u(e,!0);throw(t=new Error("Cannot find module '"+e+"'")).code="MODULE_NOT_FOUND",t}r=a[e]={exports:{}},i[e][0].call(r.exports,function(t){return o(i[e][1][t]||t)},r,r.exports,n,i,a,f)}return a[e].exports}for(var u="function"==typeof require&&require,t=0;t<f.length;t++)o(f[t]);return o}({1:[function(xzt,Pzt,t){!function(Azt){!function(){var t;!function(e){function t(){var t=this||self;t.globalThis=t,delete e.prototype._T_}"object"!=typeof globalThis&&(this?t():(e.defineProperty(e.prototype,"_T_",{configurable:!0,get:t}),_T_))}(Object),(t=globalThis).cpdf_zlib=xzt("zlib"),t.cpdf_crypto=xzt("crypto"),t.cpdf_sjcl=xzt("./sjcl.js"),function(o){"use strict";var i="object"==typeof Pzt&&Pzt.exports||o,u=83809507,e="/RunLengthDecode",r="/Oslash",a="addContent",p=192,f="add",B=365180284,s="/scaron",c="EMFILE",l="noHqPrint",d="textToPDFMemory",b="leftJustify",v="/h",m="cpdfutil.ml",_="/asciicircum",G=72,k=2147483645,S="sc",E="setAuthorXMP",D=163,N="Pdfflate.Error",F="title",j="/Encrypt",L="/numbersign",U=122,z="PMAXY",q="upright",W=181,H="/bullet",K="endstream",Ht=128,X="difference",Z="/twosuperior",$="/U",Q=45,J="stampExtended",tt="timesBold",et="ptOfIn",rt=719944127,nt="squeezeInMemory",it=1027,at=297,ft="/OCGs",ot="/acute",ht="EINPROGRESS",lt="BDC",dt="/ViewerPreferences",pt="/Ucircumflex",bt="combinePages",vt="/daggerdbl",mt="fromJSON",yt="enumeratePDFsInfo",gt="/Trapped",wt="DP",_t="/Ecircumflex",kt="/Ascent",St="/FontFile",Et="False",Mt="endGetOCGList",At="Q Deficit was nonzero. Fixing. %i\n%!",xt="/yen",Rt="numberFonts",Kt="/Scaron",Xt="UseThumbs",Zt="\nendobj\n",$t="/A",Yt="/Agrave",Vt=-43,Qt="squeeze_form_xobject",Jt="Bad Op_gs",te="getProducerXMP",ee="getPageLabelStyle",re="setMetadataFromByteArray",ne="/yacute",ie="twoPageRight",ae="getCropBox",fe="a3landscape",oe="/AE",ue="attachFileToPageFromMemory",se="/Contents",ce=100,he=111,le=180,de="setAuthor",pe="Keywords",be="/CPDFJSONmajorpdfversion",ve=-100,me="/questiondown",ye=229,ge="/FontMatrix",we="@])",_e="scaleToFit",ke="ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ\0\0\0\0ÿÿ\0\0\0\0ÿÿ\0ÿÿ\0E9\0ÿÿEÿÿÿÿÿÿÿÿ}\0\0ÿÿ\0\0ÿÿ\0\0\0©\0\0®\0ÿÿ\0\0\nE\f\0\0\n\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0s\0\0\0}\0\0\0ì\0ÿ&\0ÿÿ\n\0\0f\0:\0\0\0k\0f\0ÿÿ\v\0\0\0\v\0\v\0\v\0\v\0\v\0\v\0\v\0\v\0\v\0\v\0\0&\0\0\0o\0Ð\0é\0ÿ\0\f\0\0<\0\v\0\n\0\0\0\0\0\0 \0\"\0\0\0\0\0\0\0!\0\0\0\0\0\0\0#\0(\0$\0&\0\0\0)\0*\0+\0,\0-\0.\0:\0R\0\v\0\r\0\r\0\r\0\r\0\r\0\r\0\r\0\r\0\r\0\r\0'\0?\0'\0'\0'\0'\0'\0'\0'\0'\0'\0'\0U\0\0<\0\r\0\0\0\x000\0\x000\0\0'\x000\x000\x000\x000\x000\x000\x000\x000\x000\x000\x001\x001\x001\x001\x001\x001\x001\x001\x001\x001\x002\x002\x002\x002\x002\x002\x002\x002\x002\x002\0A\0'\0\0\0\0?\0\x003\0\x003\0\x002\x003\x003\x003\x003\x003\x003\x003\x003\x003\x003\x004\x004\x004\x004\x004\x004\x004\x004\x004\x004\x005\x005\x005\x005\x005\x005\x005\x005\x005\x005\0\x002\x006\x006\x006\x006\x006\x006\x006\x006\x006\x006\0¡\0¢\0\0[\0A\0\0\x007\x007\x007\x007\x007\x007\x007\x007\x007\x007\x009\0D\0f\0k\0s\0\0
\0
\0}\0\0
\0£\0^\0¥\0D\0¦\0§\0¨\0«\0o\0¬\0\0Î\0Ë\0Ï\0Ò\0Ó\0:\0R\0
\0Ô\0Õ\0Ö\0×\0Ù\0\0Ú\0a\0Û\0Ü\0w\0Ý\0Þ\0ß\0
\0[\0Ë\0\">é\0\0P÷\0<\0û\x006:QD\0)RSD\0w\0D\0^\0TUVGXD\0Ë\x002GD\0YD\0D\0G\0G\0G\0G\0G\0G\0G\0G\0G\0G\0a\0Lw\0Z?\0\\G\0G\0G\0G\0G\0G\0N\0N\0N\0N\0N\0N\0N\0N\0N\0N\0\0L]_ab-N\0N\0N\0N\0N\0N\0c\0dG\0G\0G\0G\0G\0G\0´\0´\0´\0´\0´\0´\0´\0´\0´\0´\0LA\0efhN\0N\0N\0N\0N\0N\0O\0O\0O\0O\0O\0O\0O\0O\0O\0O\0ij-$klmO\0O\0O\0O\0O\0O\0P\0P\0P\0P\0P\0P\0P\0P\0P\0P\0nyP\0P\0P\0P\0P\0P\0[\0>O\0O\0O\0O\0O\0O\0÷\0 û\0¡:D\0V\0V\0V\0V\0V\0V\0V\0V\0V\0V\0^\0P\0P\0P\0P\0P\0P\0V\0V\0V\0V\0V\0V\0W\0W\0W\0W\0W\0W\0W\0W\0W\0W\0$)a\0¢£w\0W\0W\0W\0W\0W\0W\0¥6\0V\0V\0V\0V\0V\0V\0¦§¨©X\0X\0X\0X\0X\0X\0X\0X\0X\0X\x002W\0W\0W\0W\0W\0W\0X\0X\0X\0X\0X\0X\0Y\0Y\0Y\0Y\0Y\0Y\0Y\0Y\0Y\0Y\0_\0
ª«
¬Y\0Y\0Y\0Y\0Y\0Y\0_\0°\0X\0X\0X\0X\0X\0X\0-®¯°\0°°\0°\0°\0°\0°\0°\0°\0°\0°\0°\0zY\0Y\0Y\0Y\0Y\0Y\0±²b\0³´µ¶·ØÁ_\0ØÍb\0Þ_\0Íå_\0Ú$××Ú×_\0Ø_\0_\0_\0`\0`\0`\0`\0`\0`\0`\0`\0`\0`\0×z\b\t\n\v`\0`\0`\0`\0`\0`\0b\0\f×÷\rb\0}b\0Üûb\0yÂb\0b\0b\0`\0`\0`\0`\0`\0`\0c\0c\0c\0c\0c\0c\0c\0c\0c\0c\0çîûîc\0c\0c\0c\0c\0c\0d\0d\0d\0d\0d\0d\0d\0d\0d\0d\0ó}àÅd\0d\0d\0d\0d\0d\0Âc\0c\0c\Line truncated
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 26C0 11.6406 11.6406 0 26 0C40.3594 0 52 11.6406 52 26C52 40.3594 40.3594 52 26 52C11.6406 52 0 40.3594 0 26Z" fill="#0C4776"/>
|
||||||
|
<path d="M26 46C37.0457 46 46 37.0457 46 26C46 14.9543 37.0457 6 26 6C14.9543 6 6 14.9543 6 26C6 37.0457 14.9543 46 26 46Z" fill="white" stroke="#007A7F" stroke-width="2"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.9398 16.7218L24.2279 18.9983L31.7583 11.5073L34.6951 14.4291L24.2279 24.8407L19.043 19.683L21.9398 16.7218Z" fill="#007A7F"/>
|
||||||
|
<path d="M18.1522 35.9595C19.0079 35.9595 19.6653 35.8029 19.9574 35.6568L19.7174 34.4151C19.4044 34.5403 18.8722 34.6447 18.3922 34.6447C16.9731 34.6447 16.1383 33.7577 16.1383 32.349C16.1383 30.7838 17.1192 30.0221 18.3818 30.0221C18.9453 30.0221 19.394 30.1473 19.7174 30.2829L20.0409 29.0203C19.7592 28.8742 19.1331 28.7073 18.3087 28.7073C16.1801 28.7073 14.4688 30.0429 14.4688 32.4429C14.4688 34.4464 15.7209 35.9595 18.1522 35.9595ZM23.8989 35.9595C24.7545 35.9595 25.4119 35.8029 25.7041 35.6568L25.4641 34.4151C25.1511 34.5403 24.6189 34.6447 24.1389 34.6447C22.7198 34.6447 21.885 33.7577 21.885 32.349C21.885 30.7838 22.8658 30.0221 24.1285 30.0221C24.6919 30.0221 25.1406 30.1473 25.4641 30.2829L25.7876 29.0203C25.5059 28.8742 24.8798 28.7073 24.0554 28.7073C21.9267 28.7073 20.2154 30.0429 20.2154 32.4429C20.2154 34.4464 21.4676 35.9595 23.8989 35.9595ZM28.1743 35.8551V33.3403C28.3203 33.3612 28.5082 33.3716 28.7169 33.3716C29.656 33.3716 30.4595 33.1421 31.0021 32.6308C31.4195 32.2342 31.649 31.6499 31.649 30.9612C31.649 30.2725 31.3464 29.6881 30.8977 29.3334C30.4282 28.9577 29.729 28.7699 28.7482 28.7699C27.7777 28.7699 27.089 28.8325 26.5986 28.916V35.8551H28.1743ZM28.7064 32.1508C28.4769 32.1508 28.3099 32.1403 28.1743 32.109V30.0325C28.289 30.0012 28.5082 29.9699 28.8316 29.9699C29.6247 29.9699 30.0734 30.356 30.0734 31.0029C30.0734 31.7229 29.5516 32.1508 28.7064 32.1508ZM33.0235 35.8551L33.5244 34.0499H35.5383L36.0809 35.8551H37.7922L35.6114 28.8221H33.5244L31.3748 35.8551H33.0235ZM35.3087 32.8603H33.754L34.1714 31.3681C34.2861 30.9612 34.3905 30.429 34.4948 30.0116H34.5157C34.6201 30.429 34.7453 30.9508 34.8705 31.3681L35.3087 32.8603Z" fill="#0C4776"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 26C0 11.6406 11.6406 0 26 0C40.3594 0 52 11.6406 52 26C52 40.3594 40.3594 52 26 52C11.6406 52 0 40.3594 0 26Z" fill="#0047BD"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.3149 11.2327L26.0039 9.98946L27.693 11.2327L27.0509 9.23793L28.733 7.99473H26.653L26.0109 6L25.3618 7.99473H23.2749L24.957 9.23793L24.3149 11.2327ZM34.6372 12.3214L32.9412 13.5646L33.5903 11.5698L31.9012 10.3266H33.9951L34.6372 8.33189L35.2863 10.3266H37.3593L35.6842 11.5698L36.3263 13.5646L34.6372 12.3214ZM39.2647 19.928L40.9607 18.6918L42.6498 19.928L42.0076 17.9332L43.6827 16.6971H41.6098L40.9607 14.7023L40.3186 16.6971H38.2247L39.9138 17.9332L39.2647 19.928ZM7.03298 28.6093L8.72902 27.3731L10.4181 28.6093L9.77596 26.6216L11.4511 25.3784H9.37812L8.736 23.3836L8.0869 25.3784H6L7.68208 26.6216L7.03298 28.6093ZM9.34325 37.3046L11.0393 36.0684L12.7283 37.3046L12.0862 35.3099L13.7683 34.0737H11.6884L11.0393 32.079L10.3972 34.0737H8.31027L9.99235 35.3099L9.34325 37.3046ZM17.3702 42.4249L15.6811 43.6681L16.3233 41.6734L14.6412 40.4302H16.7281L17.3772 38.4355L18.0193 40.4302H20.0992L18.4171 41.6734L19.0593 43.6681L17.3702 42.4249ZM24.3149 46L26.0039 44.7568L27.693 46L27.0509 44.0052L28.733 42.7691H26.653L26.0109 40.7743L25.3618 42.7691H23.2749L24.957 44.0052L24.3149 46ZM34.6372 42.4249L32.9412 43.6681L33.5903 41.6734L31.9012 40.4302H33.9951L34.6372 38.4355L35.2863 40.4302H37.3593L35.6842 41.6734L36.3263 43.6681L34.6372 42.4249ZM39.2647 37.3046L40.9607 36.0684L42.6498 37.3046L42.0076 35.3099L43.6827 34.0737H41.6098L40.9607 32.079L40.3186 34.0737H38.2247L39.9138 35.3099L39.2647 37.3046ZM43.2779 27.3731L41.5819 28.6093L42.231 26.6216L40.5489 25.3784H42.6358L43.2849 23.3836L43.927 25.3784H46L44.3249 26.6216L44.967 28.6093L43.2779 27.3731ZM17.3702 12.3214L15.6811 13.5646L16.3233 11.5698L14.6412 10.3266H16.7281L17.3772 8.33189L18.0193 10.3266H20.0992L18.4171 11.5698L19.0593 13.5646L17.3702 12.3214ZM9.34325 19.928L11.0393 18.6918L12.7283 19.928L12.0862 17.9332L13.7683 16.6971H11.6884L11.0393 14.7023L10.3972 16.6971H8.31027L9.99235 17.9332L9.34325 19.928Z" fill="#FFCC00"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.691 26.8367V25.8391H21.2723V28.0538C21.2723 29.1711 20.6973 29.8096 19.5883 29.8096C18.4793 29.8096 17.9043 29.1711 17.9043 28.0538V24.4225C17.9043 23.3052 18.4793 22.6667 19.5883 22.6667C20.6973 22.6667 21.2723 23.3052 21.2723 24.4225V25.1009H20.2044V24.3527C20.2044 23.8539 19.9785 23.6644 19.6191 23.6644C19.2597 23.6644 19.0338 23.8539 19.0338 24.3527V28.1237C19.0338 28.6225 19.2597 28.802 19.6191 28.802C19.9785 28.802 20.2044 28.6225 20.2044 28.1237V26.8367H19.691ZM22.3165 29.7297V22.7465H24.1032C25.2327 22.7465 25.7872 23.355 25.7872 24.4724V28.0039C25.7872 29.1212 25.2327 29.7297 24.1032 29.7297H22.3165ZM24.0832 23.7441H23.4465V28.7321H24.0832C24.4425 28.7321 24.6582 28.5526 24.6582 28.0538V24.4225C24.6582 23.9237 24.4425 23.7441 24.0832 23.7441ZM28.4852 22.7465C29.6147 22.7465 30.1692 23.355 30.1692 24.4724V25.3802C30.1692 26.4975 29.6147 27.106 28.4852 27.106H27.9512V29.7297H26.8217V22.7465H28.4852ZM28.4848 23.7441H27.9508V26.1084H28.4848C28.8441 26.1084 29.0392 25.9488 29.0392 25.45V24.4025C29.0392 23.9037 28.8441 23.7441 28.4848 23.7441ZM34.5708 29.7297H33.4208C33.3592 29.5502 33.3181 29.4404 33.3181 28.8718V27.7744C33.3181 27.126 33.0922 26.8866 32.5788 26.8866H32.1886V29.7297H31.0591V22.7465H32.7636C33.9342 22.7465 34.4373 23.2752 34.4373 24.3526V24.9013C34.4373 25.6196 34.2012 26.0885 33.698 26.3179C34.2628 26.5474 34.4476 27.0761 34.4476 27.8044V28.8818C34.4476 29.221 34.4579 29.4704 34.5708 29.7297ZM32.7333 23.7441H32.1891V25.889H32.6306C33.0516 25.889 33.3083 25.7094 33.3083 25.1507V24.4624C33.3083 23.9636 33.1338 23.7441 32.7333 23.7441Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="52" height="52" viewBox="0 0 52 52" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0 26C0 11.6406 11.6406 0 26 0C40.3594 0 52 11.6406 52 26C52 40.3594 40.3594 52 26 52C11.6406 52 0 40.3594 0 26Z" fill="#3D539E"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.1434 9V42.8939C22.9916 42.8918 22.8392 42.8939 22.6672 42.8939C13.4707 42.8939 6 35.3065 6 26.0655C6 16.5874 13.4707 9 22.6672 9C22.8392 9 22.9916 9.00209 23.1434 9Z" fill="white"/>
|
||||||
|
<path d="M23.1419 15.9558L20.886 17.5326L21.6983 15.0027L19.5703 13.4259H22.2105L23.1419 10.896V15.9558Z" fill="#3D539E"/>
|
||||||
|
<path d="M23.1433 15.9558L25.3992 17.5326L24.5868 15.0027L26.7148 13.4259H24.0747L23.1433 10.896V15.9558Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.2154 18.2045C41.1503 18.4463 43.26 18.5724 43.26 18.5724C43.26 18.5724 42.1094 19.5149 40.9533 19.5149C39.2311 19.5149 37.81 18.9534 37.1019 18.6736C36.9994 18.6331 36.9119 18.5985 36.8405 18.5724C35.6593 18.1398 34.3512 17.4197 33.3753 16.6066C32.3995 15.7934 31.0775 15.0338 29.9711 14.8321C28.8647 14.6304 28.1509 15.1874 27.6137 15.6688C27.3371 15.9168 27.2526 16.1548 27.109 16.5592C26.9738 16.94 26.7862 17.4684 26.3364 18.2916C25.409 19.9889 23.147 20.0365 23.147 20.0365V19.5149C23.147 19.5149 23.7836 19.5013 24.719 19.005C25.6544 18.5087 26.0527 16.942 26.2371 15.8959C26.4214 14.8498 27.1245 13.4088 28.9889 13.1967C30.8533 12.9846 32.7231 14.4067 34.3187 15.6688C35.1339 16.3136 37.1489 17.4111 40.2154 18.2045ZM28.1848 29.7639H30.3448V30.8349C30.3448 31.2061 30.3356 31.4424 30.3173 31.544C30.299 31.6455 30.2558 31.7203 30.1877 31.7683C30.1195 31.8163 29.998 31.8404 29.8231 31.8404H29.6797V31.991H31.5621V31.8404H31.4675C31.2824 31.8404 31.1563 31.8142 31.0892 31.7618C31.0221 31.7093 30.9809 31.6291 30.9656 31.521C30.9504 31.4129 30.9427 31.1842 30.9427 30.8349V28.6569C30.9427 28.2879 30.9534 28.0542 30.9748 27.956C30.9961 27.8577 31.0383 27.7906 31.1014 27.7546C31.1644 27.7185 31.2977 27.7005 31.501 27.7005H31.5621V27.5499H29.6797V27.7005H29.7895C29.999 27.7005 30.1348 27.7202 30.1968 27.7595C30.2589 27.7988 30.299 27.8697 30.3173 27.9724C30.3356 28.075 30.3448 28.3031 30.3448 28.6569V29.5379H28.1848V28.6569C28.1848 28.2879 28.1955 28.0542 28.2168 27.956C28.2382 27.8577 28.2804 27.7906 28.3434 27.7546C28.4065 27.7185 28.5387 27.7005 28.7401 27.7005H28.8163V27.5499H26.9553V27.7005H27.0316C27.2411 27.7005 27.3768 27.7202 27.4389 27.7595C27.5009 27.7988 27.5416 27.8697 27.5609 27.9724C27.5802 28.075 27.5899 28.3031 27.5899 28.6569V30.8349C27.5899 31.2061 27.5802 31.4424 27.5609 31.544C27.5416 31.6455 27.4979 31.7203 27.4297 31.7683C27.3616 31.8163 27.239 31.8404 27.0621 31.8404H26.9553V31.991H28.8163V31.8404H28.7095C28.5245 31.8404 28.3989 31.8142 28.3328 31.7618C28.2667 31.7093 28.2255 31.6291 28.2092 31.521C28.1929 31.4129 28.1848 31.1842 28.1848 30.8349V29.7639ZM31.9338 27.5499V27.7005H32.0223C32.2318 27.7005 32.3681 27.7202 32.4311 27.7595C32.4942 27.7988 32.5348 27.8697 32.5532 27.9724C32.5715 28.075 32.5806 28.3031 32.5806 28.6569V30.8349C32.5806 31.2061 32.5715 31.4424 32.5532 31.544C32.5348 31.6455 32.4916 31.7203 32.4235 31.7683C32.3554 31.8163 32.2328 31.8404 32.0559 31.8404H31.9338V31.991H33.8253V31.8404H33.7033C33.5162 31.8404 33.3896 31.8142 33.3235 31.7618C33.2574 31.7093 33.2167 31.6291 33.2015 31.521C33.1862 31.4129 33.1786 31.1842 33.1786 30.8349V28.6569C33.1786 28.2879 33.1893 28.0542 33.2106 27.956C33.232 27.8577 33.2742 27.7906 33.3372 27.7546C33.4003 27.7185 33.5335 27.7005 33.7369 27.7005H33.8253V27.5499H31.9338ZM35.4222 30.8349V29.9014H35.8768C36.3507 29.9014 36.6817 29.8534 36.8698 29.7573C37.058 29.6613 37.2024 29.5204 37.3031 29.3348C37.4037 29.1492 37.4541 28.9353 37.4541 28.6929C37.4541 28.4134 37.3804 28.182 37.2329 27.9986C37.0854 27.8151 36.8963 27.6874 36.6654 27.6154C36.4346 27.5433 36.1056 27.5073 35.6785 27.5073C35.4466 27.5073 35.1233 27.5149 34.7083 27.5302L34.1897 27.5499V27.7005H34.266C34.4755 27.7005 34.6117 27.7202 34.6748 27.7595C34.7378 27.7988 34.7785 27.8697 34.7968 27.9724C34.8151 28.075 34.8243 28.3031 34.8243 28.6569V30.8349C34.8243 31.2061 34.8151 31.4424 34.7968 31.544C34.7785 31.6455 34.7353 31.7203 34.6672 31.7683C34.599 31.8163 34.4765 31.8404 34.2995 31.8404H34.1897V31.991H36.0843V31.8404H35.947C35.7619 31.8404 35.6358 31.8142 35.5687 31.7618C35.5015 31.7093 35.4604 31.6291 35.4451 31.521C35.4299 31.4129 35.4222 31.1842 35.4222 30.8349ZM35.718 29.7376H35.4221V27.6808C35.4953 27.672 35.6011 27.6677 35.7394 27.6677C36.1157 27.6677 36.3877 27.7534 36.5555 27.9248C36.7233 28.0962 36.8072 28.3653 36.8072 28.7321C36.8072 28.9723 36.7584 29.1743 36.6608 29.338C36.5631 29.5018 36.4411 29.6093 36.2947 29.6606C36.1482 29.7119 35.956 29.7376 35.718 29.7376ZM38.6249 30.3535H40.1961L40.5134 31.1755C40.6069 31.4135 40.6537 31.5566 40.6537 31.6046C40.6537 31.6985 40.6186 31.7613 40.5484 31.7929C40.4783 31.8246 40.3435 31.8404 40.1442 31.8404V31.9911H41.7346V31.8404L41.7488 31.8379L41.7703 31.8404V31.9911H42.9641V31.8404C42.8319 31.8404 42.7236 31.806 42.6392 31.7372C42.5548 31.6685 42.5126 31.5675 42.5126 31.4343C42.5126 31.3098 42.5919 31.0435 42.7505 30.6351L42.8573 30.3535H44.4285L44.7458 31.1755C44.8393 31.4135 44.8861 31.5566 44.8861 31.6046C44.8861 31.6985 44.851 31.7613 44.7809 31.7929C44.7107 31.8246 44.576 31.8404 44.3766 31.8404V31.9911H46.0027V31.8404C45.8583 31.8251 45.75 31.7875 45.6778 31.7274C45.6056 31.6674 45.5481 31.5849 45.5054 31.4801L45.3163 31.0151L43.9343 27.4877H43.8153L42.6133 30.5696C42.5258 30.7902 42.477 30.9168 42.4668 30.9496C42.3407 31.2967 42.2329 31.5244 42.1434 31.6324C42.0563 31.7377 41.9247 31.8062 41.7488 31.8379C41.6151 31.8213 41.514 31.7845 41.4454 31.7274C41.3732 31.6674 41.31Line truncated
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.14677 17.538C8.14677 17.538 10.3838 16.9304 11.9794 15.6683C13.5751 14.4062 15.4448 12.9841 17.3092 13.1962C19.1736 13.4083 19.8767 14.8493 20.0611 15.8954C20.2454 16.9415 20.6438 18.5082 21.5791 19.0045C22.5145 19.5008 23.143 19.5145 23.143 19.5145V20.036C23.143 20.036 20.8891 19.9884 19.9617 18.2911C19.0343 16.5938 19.2216 16.1498 18.6844 15.6683C18.1472 15.1869 17.4334 14.6299 16.327 14.8316C15.2206 15.0334 13.8986 15.7929 12.9228 16.6061C11.947 17.4192 10.6388 18.1393 9.45762 18.5719C8.2764 19.0045 7.1875 19.2289 7.1875 19.2289L8.14677 17.538Z" fill="#3D539E"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.9916 42.9998C22.9916 42.9998 22.6486 42.5607 22.4081 42.3683C21.6318 41.7472 20.9942 41.1623 20.9942 40.4901C20.9942 39.3267 22.1425 39.3243 22.0677 38.7345C21.9929 38.1446 20.1119 37.5482 20.1119 35.8112C20.1119 34.3119 21.9326 33.7941 21.9326 33.1336C21.9326 32.4731 19.5805 31.9959 19.4809 30.3178C19.3714 28.4741 21.8048 27.9755 21.8048 27.5349C21.8048 26.87 17.3652 26.3046 17.3652 23.127C17.3652 21.7545 18.3777 20.9619 19.3595 20.9619C21.4626 20.9619 21.6711 22.3361 22.1444 22.6194C22.3444 22.739 22.6614 22.9048 22.8323 23.127C23.066 23.4309 23.13 23.9395 22.8323 24.2709C22.3169 24.8447 21.4177 24.334 21.3232 24.179C21.2288 24.024 20.999 23.3056 20.7376 23.0223C20.4761 22.739 19.7252 22.5377 19.3595 22.6194C18.9937 22.701 18.6295 23.127 19.0332 24.179C19.493 25.3772 23.1409 26.0546 23.1409 27.4516C23.1409 28.8486 20.5666 29.2169 20.7376 30.3932C20.9086 31.5695 23.1409 32.0379 23.1409 33.1336C23.1409 34.2293 21.2069 34.9776 21.2069 35.8895C21.2069 36.8014 23.1409 37.5954 23.1409 38.7345C23.1409 39.8736 21.9326 39.8736 21.9326 40.4901C21.9326 40.9372 22.4027 41.4877 22.7468 42.126C22.8956 42.402 22.9916 42.9998 22.9916 42.9998Z" fill="#3D539E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
+20
@@ -0,0 +1,20 @@
|
|||||||
|
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
||||||
|
|
||||||
|
interface ConvertJSONToPDFMessage {
|
||||||
|
command: 'convert';
|
||||||
|
fileBuffers: ArrayBuffer[];
|
||||||
|
fileNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JSONToPDFSuccessResponse {
|
||||||
|
status: 'success';
|
||||||
|
pdfFiles: Array<{ name: string; data: ArrayBuffer }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JSONToPDFErrorResponse {
|
||||||
|
status: 'error';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type JSONToPDFResponse = JSONToPDFSuccessResponse | JSONToPDFErrorResponse;
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
self.importScripts('/coherentpdf.browser.min.js');
|
||||||
|
|
||||||
|
function convertJSONsToPDFInWorker(fileBuffers, fileNames) {
|
||||||
|
try {
|
||||||
|
const pdfFiles = [];
|
||||||
|
const transferBuffers = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < fileBuffers.length; i++) {
|
||||||
|
const buffer = fileBuffers[i];
|
||||||
|
const fileName = fileNames[i];
|
||||||
|
const uint8Array = new Uint8Array(buffer);
|
||||||
|
|
||||||
|
let pdf;
|
||||||
|
try {
|
||||||
|
pdf = coherentpdf.fromJSONMemory(uint8Array);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error && error.message
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error';
|
||||||
|
throw new Error(
|
||||||
|
`Failed to convert "${fileName}" to PDF. ` +
|
||||||
|
`The JSON file must be in the format produced by cpdf's outputJSONMemory. ` +
|
||||||
|
`Error: ${errorMsg}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfData = coherentpdf.toMemory(pdf, false, false);
|
||||||
|
|
||||||
|
const pdfBuffer = pdfData.buffer.slice(0);
|
||||||
|
pdfFiles.push({
|
||||||
|
name: fileName,
|
||||||
|
data: pdfBuffer,
|
||||||
|
});
|
||||||
|
transferBuffers.push(pdfBuffer);
|
||||||
|
|
||||||
|
coherentpdf.deletePdf(pdf);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.postMessage(
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
pdfFiles: pdfFiles,
|
||||||
|
},
|
||||||
|
transferBuffers
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error during JSON to PDF conversion.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = (e) => {
|
||||||
|
if (e.data.command === 'convert') {
|
||||||
|
convertJSONsToPDFInWorker(e.data.fileBuffers, e.data.fileNames);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
||||||
|
|
||||||
|
interface ConvertPDFToJSONMessage {
|
||||||
|
command: 'convert';
|
||||||
|
fileBuffers: ArrayBuffer[];
|
||||||
|
fileNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PDFToJSONSuccessResponse {
|
||||||
|
status: 'success';
|
||||||
|
jsonFiles: Array<{ name: string; data: ArrayBuffer }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PDFToJSONErrorResponse {
|
||||||
|
status: 'error';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PDFToJSONResponse = PDFToJSONSuccessResponse | PDFToJSONErrorResponse;
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
self.importScripts('/coherentpdf.browser.min.js');
|
||||||
|
|
||||||
|
function convertPDFsToJSONInWorker(fileBuffers, fileNames) {
|
||||||
|
try {
|
||||||
|
const jsonFiles = [];
|
||||||
|
const transferBuffers = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < fileBuffers.length; i++) {
|
||||||
|
const buffer = fileBuffers[i];
|
||||||
|
const fileName = fileNames[i];
|
||||||
|
const uint8Array = new Uint8Array(buffer);
|
||||||
|
const pdf = coherentpdf.fromMemory(uint8Array, '');
|
||||||
|
|
||||||
|
//TODO:@ALAM -> add options for users to select these settings
|
||||||
|
// parse_content: true, no_stream_data: false, decompress_streams: false
|
||||||
|
const jsonData = coherentpdf.outputJSONMemory(true, false, false, pdf);
|
||||||
|
|
||||||
|
const jsonBuffer = jsonData.buffer.slice(0);
|
||||||
|
jsonFiles.push({
|
||||||
|
name: fileName,
|
||||||
|
data: jsonBuffer,
|
||||||
|
});
|
||||||
|
transferBuffers.push(jsonBuffer);
|
||||||
|
|
||||||
|
coherentpdf.deletePdf(pdf);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.postMessage(
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
jsonFiles: jsonFiles,
|
||||||
|
},
|
||||||
|
transferBuffers
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error during PDF to JSON conversion.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = (e) => {
|
||||||
|
if (e.data.command === 'convert') {
|
||||||
|
convertPDFsToJSONInWorker(e.data.fileBuffers, e.data.fileNames);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
declare const coherentpdf: typeof import('../../src/types/coherentpdf.global').coherentpdf;
|
||||||
|
|
||||||
|
interface GenerateTOCMessage {
|
||||||
|
command: 'generate-toc';
|
||||||
|
pdfData: ArrayBuffer;
|
||||||
|
title: string;
|
||||||
|
fontSize: number;
|
||||||
|
fontFamily: number;
|
||||||
|
addBookmark: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TOCSuccessResponse {
|
||||||
|
status: 'success';
|
||||||
|
pdfBytes: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TOCErrorResponse {
|
||||||
|
status: 'error';
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TOCResponse = TOCSuccessResponse | TOCErrorResponse;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
self.importScripts('/coherentpdf.browser.min.js');
|
||||||
|
|
||||||
|
function generateTableOfContentsInWorker(
|
||||||
|
pdfData,
|
||||||
|
title,
|
||||||
|
fontSize,
|
||||||
|
fontFamily,
|
||||||
|
addBookmark
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const uint8Array = new Uint8Array(pdfData);
|
||||||
|
const pdf = coherentpdf.fromMemory(uint8Array, '');
|
||||||
|
|
||||||
|
coherentpdf.startGetBookmarkInfo(pdf);
|
||||||
|
const bookmarkCount = coherentpdf.numberBookmarks();
|
||||||
|
coherentpdf.endGetBookmarkInfo();
|
||||||
|
|
||||||
|
if (bookmarkCount === 0) {
|
||||||
|
coherentpdf.deletePdf(pdf);
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
'This PDF does not have any bookmarks. Please add bookmarks first using the Bookmark tool.',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
coherentpdf.tableOfContents(pdf, fontFamily, fontSize, title, addBookmark);
|
||||||
|
const outputBytes = coherentpdf.toMemory(pdf, false, false);
|
||||||
|
const outputBytesBuffer = outputBytes.buffer;
|
||||||
|
coherentpdf.deletePdf(pdf);
|
||||||
|
|
||||||
|
self.postMessage(
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
pdfBytes: outputBytesBuffer,
|
||||||
|
},
|
||||||
|
[outputBytesBuffer]
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
status: 'error',
|
||||||
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Unknown error occurred during table of contents generation.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = (e) => {
|
||||||
|
if (e.data.command === 'generate-toc') {
|
||||||
|
generateTableOfContentsInWorker(
|
||||||
|
e.data.pdfData,
|
||||||
|
e.data.title,
|
||||||
|
e.data.fontSize,
|
||||||
|
e.data.fontFamily,
|
||||||
|
e.data.addBookmark
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
+1
-1
@@ -37,7 +37,7 @@ function processFile(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function processDirectory(dirPath) {
|
function processDirectory(dirPath) {
|
||||||
fs.readdirSync(dirPath).forEach(file => {
|
fs.readdirSync(dirPath).forEach((file) => {
|
||||||
const filePath = path.join(dirPath, file);
|
const filePath = path.join(dirPath, file);
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const packageJsonPath = path.join(__dirname, '../package.json');
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
|
||||||
|
function getCurrentVersion() {
|
||||||
|
return packageJson.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVersion(type) {
|
||||||
|
const currentVersion = getCurrentVersion();
|
||||||
|
const [major, minor, patch] = currentVersion.split('.').map(Number);
|
||||||
|
|
||||||
|
let newVersion;
|
||||||
|
switch (type) {
|
||||||
|
case 'major':
|
||||||
|
newVersion = `${major + 1}.0.0`;
|
||||||
|
break;
|
||||||
|
case 'minor':
|
||||||
|
newVersion = `${major}.${minor + 1}.0`;
|
||||||
|
break;
|
||||||
|
case 'patch':
|
||||||
|
default:
|
||||||
|
newVersion = `${major}.${minor}.${patch + 1}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
packageJson.version = newVersion;
|
||||||
|
fs.writeFileSync(
|
||||||
|
packageJsonPath,
|
||||||
|
JSON.stringify(packageJson, null, 2) + '\n'
|
||||||
|
);
|
||||||
|
return newVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGitTag(version) {
|
||||||
|
const tagName = `v${version}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if tag already exists
|
||||||
|
execSync(`git rev-parse "v${version}" >/dev/null 2>&1`, {
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
console.log(`✅ Tag v${version} already exists`);
|
||||||
|
return tagName;
|
||||||
|
} catch {
|
||||||
|
// Tag doesn't exist, create it
|
||||||
|
execSync(`git tag -a "v${version}" -m "Release v${version}"`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
console.log(`✅ Created tag v${version}`);
|
||||||
|
return tagName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const type = process.argv[2] || 'patch';
|
||||||
|
|
||||||
|
if (!['major', 'minor', 'patch'].includes(type)) {
|
||||||
|
console.error('❌ Invalid version type. Use: major, minor, or patch');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🚀 Releasing ${type} version...`);
|
||||||
|
|
||||||
|
// 1. Update version in package.json
|
||||||
|
const newVersion = updateVersion(type);
|
||||||
|
console.log(`📦 Updated version to ${newVersion}`);
|
||||||
|
|
||||||
|
// 2. Add and commit changes
|
||||||
|
execSync('git add package.json', { stdio: 'inherit' });
|
||||||
|
execSync(`git commit -m "Release v${newVersion}"`, { stdio: 'inherit' });
|
||||||
|
console.log(`💾 Committed version change`);
|
||||||
|
|
||||||
|
// 3. Create git tag
|
||||||
|
const tagName = createGitTag(newVersion);
|
||||||
|
|
||||||
|
// 4. Push everything to main
|
||||||
|
console.log(`📤 Pushing to main...`);
|
||||||
|
execSync('git push origin main', { stdio: 'inherit' });
|
||||||
|
execSync(`git push origin ${tagName}`, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
console.log(`🎉 Release v${newVersion} complete!`);
|
||||||
|
console.log(`📦 Docker image: bentopdf/bentopdf:${newVersion}`);
|
||||||
|
console.log(
|
||||||
|
`🏷️ GitHub release: https://github.com/alam00000/bentopdf/releases/tag/${tagName}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
body {
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-ghost {
|
||||||
|
opacity: 0.4;
|
||||||
|
border: 2px dashed #2b7fff;
|
||||||
|
}
|
||||||
|
.sortable-drag {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-minimized {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
max-width: 350px;
|
||||||
|
max-height: none;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-container {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-container::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0.75rem;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-container > li {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.child-container > li::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -0.75rem;
|
||||||
|
top: 1.25rem;
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 2px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-preview {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination-marker {
|
||||||
|
position: absolute;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin-left: -15px;
|
||||||
|
margin-top: -15px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination-marker svg {
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-canvas-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-canvas-wrapper.picking-mode {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinate-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 20;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.destination-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
border: 2px solid #3b82f6;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
+63
-9
@@ -1,4 +1,48 @@
|
|||||||
@import "tailwindcss";
|
@import '@fontsource/dm-sans/400.css';
|
||||||
|
@import '@fontsource/dm-sans/500.css';
|
||||||
|
@import '@fontsource/dm-sans/600.css';
|
||||||
|
@import '@fontsource/dm-sans/700.css';
|
||||||
|
@import '@fontsource/dancing-script/400.css';
|
||||||
|
@import '@fontsource/great-vibes/400.css';
|
||||||
|
@import '@fontsource/kalam/300.css';
|
||||||
|
@import '@fontsource/kalam/400.css';
|
||||||
|
@import '@fontsource/kalam/700.css';
|
||||||
|
@import '@fontsource/lato/400.css';
|
||||||
|
@import '@fontsource/lato/700.css';
|
||||||
|
@import '@fontsource/lato/400-italic.css';
|
||||||
|
@import '@fontsource/merriweather/400.css';
|
||||||
|
@import '@fontsource/merriweather/700.css';
|
||||||
|
@import '@fontsource/merriweather/400-italic.css';
|
||||||
|
@import '@fontsource/cedarville-cursive/400.css';
|
||||||
|
@import 'cropperjs/dist/cropper.css';
|
||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-dancing: 'Dancing Script', cursive;
|
||||||
|
--font-vibes: 'Great Vibes', cursive;
|
||||||
|
--font-kalam: 'Kalam', cursive;
|
||||||
|
--font-lato: 'Lato', sans-serif;
|
||||||
|
--font-merriweather: 'Merriweather', serif;
|
||||||
|
--font-cedarville: 'Cedarville Cursive', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply antialiased;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: 'DM Sans', ui-sans-serif, system-ui;
|
||||||
|
--font-dancing: 'Dancing Script', cursive;
|
||||||
|
--font-vibes: 'Great Vibes', cursive;
|
||||||
|
--font-kalam: 'Kalam', cursive;
|
||||||
|
--font-lato: 'Lato', sans-serif;
|
||||||
|
--font-merriweather: 'Merriweather', serif;
|
||||||
|
--font-cedarville: 'Cedarville Cursive', cursive;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
@@ -11,18 +55,24 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-card {
|
.tool-card {
|
||||||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
transition:
|
||||||
|
transform 0.2s ease-in-out,
|
||||||
|
box-shadow 0.2s ease-in-out;
|
||||||
border: 1px solid #374151;
|
border: 1px solid #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-card:hover {
|
.tool-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.2),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.1);
|
||||||
border-color: #4f46e5;
|
border-color: #4f46e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out;
|
transition:
|
||||||
|
background-color 0.2s ease-in-out,
|
||||||
|
transform 0.1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:active {
|
.btn:active {
|
||||||
@@ -30,7 +80,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Custom file input */
|
/* Custom file input */
|
||||||
input[type="file"]::file-selector-button {
|
input[type='file']::file-selector-button {
|
||||||
@apply bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors duration-200 mr-4;
|
@apply bg-indigo-600 text-white font-semibold py-2 px-4 rounded-lg cursor-pointer hover:bg-indigo-700 transition-colors duration-200 mr-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +115,7 @@ input[type="file"]::file-selector-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#tool-interface {
|
#tool-interface {
|
||||||
color: #39A0ED;
|
color: #39a0ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-thumbnail,
|
.page-thumbnail,
|
||||||
@@ -197,7 +247,7 @@ footer a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.marker-slanted::before {
|
.marker-slanted::before {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -236,7 +286,9 @@ footer a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-gradient:hover {
|
.btn-gradient:hover {
|
||||||
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3), 0 4px 6px -4px rgba(79, 70, 229, 0.3);
|
box-shadow:
|
||||||
|
0 10px 15px -3px rgba(79, 70, 229, 0.3),
|
||||||
|
0 4px 6px -4px rgba(79, 70, 229, 0.3);
|
||||||
/* hover:shadow-xl hover:shadow-indigo-500/30 */
|
/* hover:shadow-xl hover:shadow-indigo-500/30 */
|
||||||
transform: translateY(-0.25rem);
|
transform: translateY(-0.25rem);
|
||||||
/* hover:-translate-y-1 */
|
/* hover:-translate-y-1 */
|
||||||
@@ -245,7 +297,9 @@ footer a {
|
|||||||
.btn-gradient:focus {
|
.btn-gradient:focus {
|
||||||
outline: 2px solid transparent;
|
outline: 2px solid transparent;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
box-shadow: 0 0 0 2px #111827, 0 0 0 4px #818cf8;
|
box-shadow:
|
||||||
|
0 0 0 2px #111827,
|
||||||
|
0 0 0 4px #818cf8;
|
||||||
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
|
/* focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-900 focus:ring-indigo-400 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-18
@@ -1,7 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from './ui.js';
|
import { showLoader, hideLoader, showAlert } from './ui.js';
|
||||||
import { state } from './state.js';
|
import { state } from './state.js';
|
||||||
import { toolLogic } from './logic/index.js';
|
import { toolLogic } from './logic/index.js';
|
||||||
import { icons, createIcons } from "lucide";
|
import { icons, createIcons } from 'lucide';
|
||||||
|
|
||||||
const editorState = {
|
const editorState = {
|
||||||
pdf: null,
|
pdf: null,
|
||||||
@@ -52,16 +52,20 @@ async function renderPage(num: any) {
|
|||||||
|
|
||||||
const renderContext = {
|
const renderContext = {
|
||||||
canvasContext: editorState.context,
|
canvasContext: editorState.context,
|
||||||
viewport: viewport
|
viewport: viewport,
|
||||||
};
|
};
|
||||||
|
|
||||||
await page.render(renderContext).promise;
|
await page.render(renderContext).promise;
|
||||||
|
|
||||||
editorState.pageSnapshot = editorState.context.getImageData(0, 0, editorState.canvas.width, editorState.canvas.height);
|
editorState.pageSnapshot = editorState.context.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
editorState.canvas.width,
|
||||||
|
editorState.canvas.height
|
||||||
|
);
|
||||||
redrawShapes();
|
redrawShapes();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error rendering page:", error);
|
console.error('Error rendering page:', error);
|
||||||
showAlert('Render Error', 'Could not display the page.');
|
showAlert('Render Error', 'Could not display the page.');
|
||||||
} finally {
|
} finally {
|
||||||
editorState.pageRendering = false;
|
editorState.pageRendering = false;
|
||||||
@@ -71,7 +75,8 @@ async function renderPage(num: any) {
|
|||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('prev-page').disabled = num <= 1;
|
document.getElementById('prev-page').disabled = num <= 1;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('next-page').disabled = num >= editorState.pdf.numPages;
|
document.getElementById('next-page').disabled =
|
||||||
|
num >= editorState.pdf.numPages;
|
||||||
|
|
||||||
if (editorState.pageNumPending !== null) {
|
if (editorState.pageNumPending !== null) {
|
||||||
const pendingPage = editorState.pageNumPending;
|
const pendingPage = editorState.pageNumPending;
|
||||||
@@ -100,7 +105,12 @@ function redrawShapes() {
|
|||||||
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
editorState.context.strokeStyle = 'rgba(79, 70, 229, 0.9)';
|
||||||
editorState.context.lineWidth = 2;
|
editorState.context.lineWidth = 2;
|
||||||
editorState.context.setLineDash([8, 4]);
|
editorState.context.setLineDash([8, 4]);
|
||||||
editorState.context.strokeRect(currentCropBox.x, currentCropBox.y, currentCropBox.width, currentCropBox.height);
|
editorState.context.strokeRect(
|
||||||
|
currentCropBox.x,
|
||||||
|
currentCropBox.y,
|
||||||
|
currentCropBox.width,
|
||||||
|
currentCropBox.height
|
||||||
|
);
|
||||||
editorState.context.setLineDash([]);
|
editorState.context.setLineDash([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +170,7 @@ function handleInteractionEnd() {
|
|||||||
|
|
||||||
editorState.cropBoxes[editorState.currentPageNum - 1] = {
|
editorState.cropBoxes[editorState.currentPageNum - 1] = {
|
||||||
...finalRect,
|
...finalRect,
|
||||||
scale: editorState.scale
|
scale: editorState.scale,
|
||||||
};
|
};
|
||||||
|
|
||||||
editorState.lastInteractionRect = null; // Reset for the next drawing action
|
editorState.lastInteractionRect = null; // Reset for the next drawing action
|
||||||
@@ -187,7 +197,8 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
|
|
||||||
const prevButton = document.createElement('button');
|
const prevButton = document.createElement('button');
|
||||||
prevButton.id = 'prev-page';
|
prevButton.id = 'prev-page';
|
||||||
prevButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
prevButton.className =
|
||||||
|
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||||
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
|
prevButton.innerHTML = '<i data-lucide="chevron-left"></i>';
|
||||||
|
|
||||||
const pageInfo = document.createElement('span');
|
const pageInfo = document.createElement('span');
|
||||||
@@ -197,11 +208,16 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
currentPageDisplay.id = 'current-page-display';
|
currentPageDisplay.id = 'current-page-display';
|
||||||
currentPageDisplay.textContent = '1';
|
currentPageDisplay.textContent = '1';
|
||||||
|
|
||||||
pageInfo.append('Page ', currentPageDisplay, ` of ${editorState.pdf.numPages}`);
|
pageInfo.append(
|
||||||
|
'Page ',
|
||||||
|
currentPageDisplay,
|
||||||
|
` of ${editorState.pdf.numPages}`
|
||||||
|
);
|
||||||
|
|
||||||
const nextButton = document.createElement('button');
|
const nextButton = document.createElement('button');
|
||||||
nextButton.id = 'next-page';
|
nextButton.id = 'next-page';
|
||||||
nextButton.className = 'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
nextButton.className =
|
||||||
|
'btn p-2 rounded-full bg-gray-700 hover:bg-gray-600 disabled:opacity-50';
|
||||||
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
nextButton.innerHTML = '<i data-lucide="chevron-right"></i>';
|
||||||
|
|
||||||
pageNav.append(prevButton, pageInfo, nextButton);
|
pageNav.append(prevButton, pageInfo, nextButton);
|
||||||
@@ -209,10 +225,12 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
document.getElementById('prev-page').addEventListener('click', () => {
|
document.getElementById('prev-page').addEventListener('click', () => {
|
||||||
if (editorState.currentPageNum > 1) queueRenderPage(editorState.currentPageNum - 1);
|
if (editorState.currentPageNum > 1)
|
||||||
|
queueRenderPage(editorState.currentPageNum - 1);
|
||||||
});
|
});
|
||||||
document.getElementById('next-page').addEventListener('click', () => {
|
document.getElementById('next-page').addEventListener('click', () => {
|
||||||
if (editorState.currentPageNum < editorState.pdf.numPages) queueRenderPage(editorState.currentPageNum + 1);
|
if (editorState.currentPageNum < editorState.pdf.numPages)
|
||||||
|
queueRenderPage(editorState.currentPageNum + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// To prevent stacking multiple listeners, we replace the canvas element with a clone
|
// To prevent stacking multiple listeners, we replace the canvas element with a clone
|
||||||
@@ -228,8 +246,12 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
|
editorState.canvas.addEventListener('mouseleave', handleInteractionEnd);
|
||||||
|
|
||||||
// Touch Events
|
// Touch Events
|
||||||
editorState.canvas.addEventListener('touchstart', handleInteractionStart, { passive: false });
|
editorState.canvas.addEventListener('touchstart', handleInteractionStart, {
|
||||||
editorState.canvas.addEventListener('touchmove', handleInteractionMove, { passive: false });
|
passive: false,
|
||||||
|
});
|
||||||
|
editorState.canvas.addEventListener('touchmove', handleInteractionMove, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
editorState.canvas.addEventListener('touchend', handleInteractionEnd);
|
||||||
|
|
||||||
if (toolId === 'crop') {
|
if (toolId === 'crop') {
|
||||||
@@ -259,12 +281,20 @@ export async function setupCanvasEditor(toolId: any) {
|
|||||||
|
|
||||||
document.getElementById('process-btn').onclick = async () => {
|
document.getElementById('process-btn').onclick = async () => {
|
||||||
if (Object.keys(editorState.cropBoxes).length === 0) {
|
if (Object.keys(editorState.cropBoxes).length === 0) {
|
||||||
showAlert('No Area Selected', 'Please draw a rectangle on at least one page to select the crop area.');
|
showAlert(
|
||||||
|
'No Area Selected',
|
||||||
|
'Please draw a rectangle on at least one page to select the crop area.'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const success = await toolLogic['crop-pdf'].process(editorState.cropBoxes);
|
const success = await toolLogic['crop-pdf'].process(
|
||||||
|
editorState.cropBoxes
|
||||||
|
);
|
||||||
if (success) {
|
if (success) {
|
||||||
showAlert('Success!', 'Your PDF has been cropped and the download has started.');
|
showAlert(
|
||||||
|
'Success!',
|
||||||
|
'Your PDF has been cropped and the download has started.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,68 @@
|
|||||||
export const singlePdfLoadTools = [
|
export const singlePdfLoadTools = [
|
||||||
'split', 'organize', 'rotate', 'add-page-numbers',
|
'split',
|
||||||
'pdf-to-jpg', 'pdf-to-png', 'pdf-to-webp', 'compress', 'pdf-to-greyscale',
|
'organize',
|
||||||
'edit-metadata', 'remove-metadata', 'flatten', 'delete-pages', 'add-blank-page',
|
'rotate',
|
||||||
'extract-pages', 'add-watermark', 'add-header-footer', 'invert-colors', 'view-metadata',
|
'add-page-numbers',
|
||||||
'reverse-pages', 'crop', 'redact', 'pdf-to-bmp', 'pdf-to-tiff', 'split-in-half',
|
'pdf-to-jpg',
|
||||||
'page-dimensions', 'n-up', 'duplicate-organize', 'combine-single-page', 'fix-dimensions', 'change-background-color',
|
'pdf-to-png',
|
||||||
'change-text-color', 'ocr-pdf', 'sign-pdf', 'remove-annotations', 'cropper', 'form-filler', 'posterize'
|
'pdf-to-webp',
|
||||||
|
'compress',
|
||||||
|
'pdf-to-greyscale',
|
||||||
|
'edit-metadata',
|
||||||
|
'remove-metadata',
|
||||||
|
'flatten',
|
||||||
|
'delete-pages',
|
||||||
|
'add-blank-page',
|
||||||
|
'extract-pages',
|
||||||
|
'add-watermark',
|
||||||
|
'add-header-footer',
|
||||||
|
'invert-colors',
|
||||||
|
'view-metadata',
|
||||||
|
'crop',
|
||||||
|
'redact',
|
||||||
|
'pdf-to-bmp',
|
||||||
|
'pdf-to-tiff',
|
||||||
|
'split-in-half',
|
||||||
|
'page-dimensions',
|
||||||
|
'n-up',
|
||||||
|
'duplicate-organize',
|
||||||
|
'combine-single-page',
|
||||||
|
'fix-dimensions',
|
||||||
|
'change-background-color',
|
||||||
|
'change-text-color',
|
||||||
|
'ocr-pdf',
|
||||||
|
'sign-pdf',
|
||||||
|
'remove-annotations',
|
||||||
|
'cropper',
|
||||||
|
'form-filler',
|
||||||
|
'posterize',
|
||||||
|
'remove-blank-pages',
|
||||||
|
'add-attachments',
|
||||||
|
'sanitize-pdf',
|
||||||
|
'remove-restrictions',
|
||||||
|
'bookmark-pdf',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const simpleTools = [
|
export const simpleTools = [
|
||||||
'encrypt', 'decrypt', 'change-permissions', 'pdf-to-markdown', 'word-to-pdf',
|
'encrypt',
|
||||||
|
'decrypt',
|
||||||
|
'change-permissions',
|
||||||
|
'pdf-to-markdown',
|
||||||
|
'word-to-pdf',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const multiFileTools = [
|
export const multiFileTools = [
|
||||||
'merge', 'pdf-to-zip', 'jpg-to-pdf', 'png-to-pdf', 'webp-to-pdf', 'image-to-pdf', 'svg-to-pdf', 'bmp-to-pdf', 'heic-to-pdf', 'tiff-to-pdf',
|
'merge',
|
||||||
|
'pdf-to-zip',
|
||||||
|
'jpg-to-pdf',
|
||||||
|
'png-to-pdf',
|
||||||
|
'webp-to-pdf',
|
||||||
|
'image-to-pdf',
|
||||||
|
'svg-to-pdf',
|
||||||
|
'bmp-to-pdf',
|
||||||
|
'heic-to-pdf',
|
||||||
|
'tiff-to-pdf',
|
||||||
|
'alternate-merge',
|
||||||
|
'linearize',
|
||||||
|
'reverse-pages',
|
||||||
];
|
];
|
||||||
@@ -1,22 +1,104 @@
|
|||||||
export const tesseractLanguages = {
|
export const tesseractLanguages = {
|
||||||
"eng": "English", "afr": "Afrikaans", "amh": "Amharic", "ara": "Arabic", "asm": "Assamese", "aze": "Azerbaijani",
|
eng: 'English',
|
||||||
"aze_cyrl": "Azerbaijani - Cyrillic", "bel": "Belarusian", "ben": "Bengali", "bod": "Tibetan", "bos": "Bosnian",
|
afr: 'Afrikaans',
|
||||||
"bul": "Bulgarian", "cat": "Catalan; Valencian", "ceb": "Cebuano", "ces": "Czech", "chi_sim": "Chinese - Simplified",
|
amh: 'Amharic',
|
||||||
"chi_tra": "Chinese - Traditional", "chr": "Cherokee", "cym": "Welsh", "dan": "Danish", "deu": "German",
|
ara: 'Arabic',
|
||||||
"dzo": "Dzongkha", "ell": "Greek, Modern (1453-)", "enm": "English, Middle (1100-1500)", "epo": "Esperanto",
|
asm: 'Assamese',
|
||||||
"est": "Estonian", "eus": "Basque", "fas": "Persian", "fin": "Finnish", "fra": "French", "frk": "German Fraktur",
|
aze: 'Azerbaijani',
|
||||||
"frm": "French, Middle (ca. 1400-1600)", "gle": "Irish", "glg": "Galician", "grc": "Greek, Ancient (-1453)",
|
aze_cyrl: 'Azerbaijani - Cyrillic',
|
||||||
"guj": "Gujarati", "hat": "Haitian; Haitian Creole", "heb": "Hebrew", "hin": "Hindi", "hrv": "Croatian",
|
bel: 'Belarusian',
|
||||||
"hun": "Hungarian", "iku": "Inuktitut", "ind": "Indonesian", "isl": "Icelandic", "ita": "Italian",
|
ben: 'Bengali',
|
||||||
"ita_old": "Italian - Old", "jav": "Javanese", "jpn": "Japanese", "kan": "Kannada", "kat": "Georgian",
|
bod: 'Tibetan',
|
||||||
"kat_old": "Georgian - Old", "kaz": "Kazakh", "khm": "Central Khmer", "kir": "Kirghiz; Kyrgyz",
|
bos: 'Bosnian',
|
||||||
"kor": "Korean", "kur": "Kurdish", "lao": "Lao", "lat": "Latin", "lav": "Latvian", "lit": "Lithuanian",
|
bul: 'Bulgarian',
|
||||||
"mal": "Malayalam", "mar": "Marathi", "mkd": "Macedonian", "mlt": "Maltese", "msa": "Malay", "mya": "Burmese",
|
cat: 'Catalan; Valencian',
|
||||||
"nep": "Nepali", "nld": "Dutch; Flemish", "nor": "Norwegian", "ori": "Oriya", "pan": "Panjabi; Punjabi",
|
ceb: 'Cebuano',
|
||||||
"pol": "Polish", "por": "Portuguese", "pus": "Pushto; Pashto", "ron": "Romanian; Moldavian; Moldovan",
|
ces: 'Czech',
|
||||||
"rus": "Russian", "san": "Sanskrit", "sin": "Sinhala; Sinhalese", "slk": "Slovak", "slv": "Slovenian",
|
chi_sim: 'Chinese - Simplified',
|
||||||
"spa": "Spanish; Castilian", "spa_old": "Spanish; Castilian - Old", "sqi": "Albanian", "srp": "Serbian",
|
chi_tra: 'Chinese - Traditional',
|
||||||
"srp_latn": "Serbian - Latin", "swa": "Swahili", "swe": "Swedish", "syr": "Syriac", "tam": "Tamil", "tel": "Telugu",
|
chr: 'Cherokee',
|
||||||
"tgk": "Tajik", "tgl": "Tagalog", "tha": "Thai", "tir": "Tigrinya", "tur": "Turkish", "uig": "Uighur; Uyghur",
|
cym: 'Welsh',
|
||||||
"ukr": "Ukrainian", "urd": "Urdu", "uzb": "Uzbek", "uzb_cyrl": "Uzbek - Cyrillic", "vie": "Vietnamese", "yid": "Yiddish"
|
dan: 'Danish',
|
||||||
|
deu: 'German',
|
||||||
|
dzo: 'Dzongkha',
|
||||||
|
ell: 'Greek, Modern (1453-)',
|
||||||
|
enm: 'English, Middle (1100-1500)',
|
||||||
|
epo: 'Esperanto',
|
||||||
|
est: 'Estonian',
|
||||||
|
eus: 'Basque',
|
||||||
|
fas: 'Persian',
|
||||||
|
fin: 'Finnish',
|
||||||
|
fra: 'French',
|
||||||
|
frk: 'German Fraktur',
|
||||||
|
frm: 'French, Middle (ca. 1400-1600)',
|
||||||
|
gle: 'Irish',
|
||||||
|
glg: 'Galician',
|
||||||
|
grc: 'Greek, Ancient (-1453)',
|
||||||
|
guj: 'Gujarati',
|
||||||
|
hat: 'Haitian; Haitian Creole',
|
||||||
|
heb: 'Hebrew',
|
||||||
|
hin: 'Hindi',
|
||||||
|
hrv: 'Croatian',
|
||||||
|
hun: 'Hungarian',
|
||||||
|
iku: 'Inuktitut',
|
||||||
|
ind: 'Indonesian',
|
||||||
|
isl: 'Icelandic',
|
||||||
|
ita: 'Italian',
|
||||||
|
ita_old: 'Italian - Old',
|
||||||
|
jav: 'Javanese',
|
||||||
|
jpn: 'Japanese',
|
||||||
|
kan: 'Kannada',
|
||||||
|
kat: 'Georgian',
|
||||||
|
kat_old: 'Georgian - Old',
|
||||||
|
kaz: 'Kazakh',
|
||||||
|
khm: 'Central Khmer',
|
||||||
|
kir: 'Kirghiz; Kyrgyz',
|
||||||
|
kor: 'Korean',
|
||||||
|
kur: 'Kurdish',
|
||||||
|
lao: 'Lao',
|
||||||
|
lat: 'Latin',
|
||||||
|
lav: 'Latvian',
|
||||||
|
lit: 'Lithuanian',
|
||||||
|
mal: 'Malayalam',
|
||||||
|
mar: 'Marathi',
|
||||||
|
mkd: 'Macedonian',
|
||||||
|
mlt: 'Maltese',
|
||||||
|
msa: 'Malay',
|
||||||
|
mya: 'Burmese',
|
||||||
|
nep: 'Nepali',
|
||||||
|
nld: 'Dutch; Flemish',
|
||||||
|
nor: 'Norwegian',
|
||||||
|
ori: 'Oriya',
|
||||||
|
pan: 'Panjabi; Punjabi',
|
||||||
|
pol: 'Polish',
|
||||||
|
por: 'Portuguese',
|
||||||
|
pus: 'Pushto; Pashto',
|
||||||
|
ron: 'Romanian; Moldavian; Moldovan',
|
||||||
|
rus: 'Russian',
|
||||||
|
san: 'Sanskrit',
|
||||||
|
sin: 'Sinhala; Sinhalese',
|
||||||
|
slk: 'Slovak',
|
||||||
|
slv: 'Slovenian',
|
||||||
|
spa: 'Spanish; Castilian',
|
||||||
|
spa_old: 'Spanish; Castilian - Old',
|
||||||
|
sqi: 'Albanian',
|
||||||
|
srp: 'Serbian',
|
||||||
|
srp_latn: 'Serbian - Latin',
|
||||||
|
swa: 'Swahili',
|
||||||
|
swe: 'Swedish',
|
||||||
|
syr: 'Syriac',
|
||||||
|
tam: 'Tamil',
|
||||||
|
tel: 'Telugu',
|
||||||
|
tgk: 'Tajik',
|
||||||
|
tgl: 'Tagalog',
|
||||||
|
tha: 'Thai',
|
||||||
|
tir: 'Tigrinya',
|
||||||
|
tur: 'Turkish',
|
||||||
|
uig: 'Uighur; Uyghur',
|
||||||
|
ukr: 'Ukrainian',
|
||||||
|
urd: 'Urdu',
|
||||||
|
uzb: 'Uzbek',
|
||||||
|
uzb_cyrl: 'Uzbek - Cyrillic',
|
||||||
|
vie: 'Vietnamese',
|
||||||
|
yid: 'Yiddish',
|
||||||
};
|
};
|
||||||
+443
-70
@@ -3,103 +3,476 @@ export const categories = [
|
|||||||
{
|
{
|
||||||
name: 'Popular Tools',
|
name: 'Popular Tools',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
|
{
|
||||||
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
|
id: 'merge',
|
||||||
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
|
name: 'Merge PDF',
|
||||||
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs' },
|
icon: 'combine',
|
||||||
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
|
subtitle: 'Combine multiple PDFs into one file.',
|
||||||
{ id: 'sign-pdf', name: 'Sign PDF', icon: 'pen-tool', subtitle: 'Draw, type, or upload your signature.' },
|
},
|
||||||
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
{
|
||||||
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
|
id: 'split',
|
||||||
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
|
name: 'Split PDF',
|
||||||
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
|
icon: 'scissors',
|
||||||
]
|
subtitle: 'Extract a range of pages into a new PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compress',
|
||||||
|
name: 'Compress PDF',
|
||||||
|
icon: 'zap',
|
||||||
|
subtitle: 'Reduce the file size of your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit',
|
||||||
|
name: 'PDF Editor',
|
||||||
|
icon: 'pocket-knife',
|
||||||
|
subtitle:
|
||||||
|
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'jpg-to-pdf',
|
||||||
|
name: 'JPG to PDF',
|
||||||
|
icon: 'image-up',
|
||||||
|
subtitle: 'Create a PDF from one or more JPG images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sign-pdf',
|
||||||
|
name: 'Sign PDF',
|
||||||
|
icon: 'pen-tool',
|
||||||
|
subtitle: 'Draw, type, or upload your signature.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cropper',
|
||||||
|
name: 'Crop PDF',
|
||||||
|
icon: 'crop',
|
||||||
|
subtitle: 'Trim the margins of every page in your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'extract-pages',
|
||||||
|
name: 'Extract Pages',
|
||||||
|
icon: 'ungroup',
|
||||||
|
subtitle: 'Save a selection of pages as new files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'duplicate-organize',
|
||||||
|
name: 'Duplicate & Organize',
|
||||||
|
icon: 'files',
|
||||||
|
subtitle: 'Duplicate, reorder, and delete pages.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delete-pages',
|
||||||
|
name: 'Delete Pages',
|
||||||
|
icon: 'trash-2',
|
||||||
|
subtitle: 'Remove specific pages from your document.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Edit & Annotate',
|
name: 'Edit & Annotate',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'edit', name: 'PDF Editor', icon: 'pocket-knife', subtitle: 'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.' },
|
{
|
||||||
// { id: 'crop', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
id: 'edit',
|
||||||
{ id: 'add-page-numbers', name: 'Page Numbers', icon: 'list-ordered', subtitle: 'Insert page numbers into your document.' },
|
name: 'PDF Editor',
|
||||||
{ id: 'add-watermark', name: 'Add Watermark', icon: 'droplets', subtitle: 'Stamp text or an image over your PDF pages.' },
|
icon: 'pocket-knife',
|
||||||
{ id: 'add-header-footer', name: 'Header & Footer', icon: 'pilcrow', subtitle: 'Add text to the top and bottom of pages.' },
|
subtitle:
|
||||||
{ id: 'invert-colors', name: 'Invert Colors', icon: 'contrast', subtitle: 'Create a "dark mode" version of your PDF.' },
|
'Annotate, highlight, redact, comment, add shapes/images, search, and view PDFs.',
|
||||||
{ 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: 'bookmark-pdf',
|
||||||
{ id: 'remove-annotations', name: 'Remove Annotations', icon: 'eraser', subtitle: 'Strip comments, highlights, and links.' },
|
href: '/src/pages/bookmark.html',
|
||||||
{ id: 'cropper', name: 'Crop PDF', icon: 'crop', subtitle: 'Trim the margins of every page in your PDF.' },
|
name: 'Edit Bookmarks',
|
||||||
{ id: 'form-filler', name: 'PDF Form Filler', icon: 'square-pen', subtitle: 'Fill in forms directly in the browser.' },
|
icon: 'bookmark',
|
||||||
]
|
subtitle: 'Add, edit, import, delete and extract PDF bookmarks.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/src/pages/table-of-contents.html',
|
||||||
|
name: 'Table of Contents',
|
||||||
|
icon: 'list',
|
||||||
|
subtitle: 'Generate a table of contents page from PDF bookmarks.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-blank-pages',
|
||||||
|
name: 'Remove Blank Pages',
|
||||||
|
icon: 'file-minus-2',
|
||||||
|
subtitle: 'Automatically detect and delete blank pages.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Convert to PDF',
|
name: 'Convert to PDF',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'image-to-pdf', name: 'Image to PDF', icon: 'images', subtitle: 'Combine various images into one PDF.' },
|
{
|
||||||
{ id: 'jpg-to-pdf', name: 'JPG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more JPG images.' },
|
id: 'image-to-pdf',
|
||||||
{ id: 'png-to-pdf', name: 'PNG to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more PNG images.' },
|
name: 'Image to PDF',
|
||||||
{ id: 'webp-to-pdf', name: 'WebP to PDF', icon: 'image-up', subtitle: 'Create a PDF from one or more WebP images.' },
|
icon: 'images',
|
||||||
{ id: 'svg-to-pdf', name: 'SVG to PDF', icon: 'pen-tool', subtitle: 'Create a PDF from one or more SVG images.' },
|
subtitle: 'Combine various images into one PDF.',
|
||||||
{ id: 'bmp-to-pdf', name: 'BMP to PDF', icon: 'image', subtitle: 'Create a PDF from one or more BMP images.' },
|
},
|
||||||
{ id: 'heic-to-pdf', name: 'HEIC to PDF', icon: 'smartphone', subtitle: 'Create a PDF from one or more HEIC images.' },
|
{
|
||||||
{ id: 'tiff-to-pdf', name: 'TIFF to PDF', icon: 'layers', subtitle: 'Create a PDF from one or more TIFF images.' },
|
id: 'jpg-to-pdf',
|
||||||
{ id: 'txt-to-pdf', name: 'Text to PDF', icon: 'file-pen', subtitle: 'Convert a plain text file into a PDF.' },
|
name: 'JPG to PDF',
|
||||||
|
icon: 'image-up',
|
||||||
|
subtitle: 'Create a PDF from one or more JPG images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'png-to-pdf',
|
||||||
|
name: 'PNG to PDF',
|
||||||
|
icon: 'image-up',
|
||||||
|
subtitle: 'Create a PDF from one or more PNG images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'webp-to-pdf',
|
||||||
|
name: 'WebP to PDF',
|
||||||
|
icon: 'image-up',
|
||||||
|
subtitle: 'Create a PDF from one or more WebP images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'svg-to-pdf',
|
||||||
|
name: 'SVG to PDF',
|
||||||
|
icon: 'pen-tool',
|
||||||
|
subtitle: 'Create a PDF from one or more SVG images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bmp-to-pdf',
|
||||||
|
name: 'BMP to PDF',
|
||||||
|
icon: 'image',
|
||||||
|
subtitle: 'Create a PDF from one or more BMP images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'heic-to-pdf',
|
||||||
|
name: 'HEIC to PDF',
|
||||||
|
icon: 'smartphone',
|
||||||
|
subtitle: 'Create a PDF from one or more HEIC images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tiff-to-pdf',
|
||||||
|
name: 'TIFF to PDF',
|
||||||
|
icon: 'layers',
|
||||||
|
subtitle: 'Create a PDF from one or more TIFF images.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'txt-to-pdf',
|
||||||
|
name: 'Text to PDF',
|
||||||
|
icon: 'file-pen',
|
||||||
|
subtitle: 'Convert a plain text file into a PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/src/pages/json-to-pdf.html',
|
||||||
|
name: 'JSON to PDF',
|
||||||
|
icon: 'file-code',
|
||||||
|
subtitle: 'Convert JSON files to PDF format.',
|
||||||
|
},
|
||||||
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
|
// { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
|
||||||
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
|
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
|
||||||
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
|
// { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Convert from PDF',
|
name: 'Convert from PDF',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'pdf-to-jpg', name: 'PDF to JPG', icon: 'file-image', subtitle: 'Convert each PDF page into a JPG image.' },
|
{
|
||||||
{ id: 'pdf-to-png', name: 'PDF to PNG', icon: 'file-image', subtitle: 'Convert each PDF page into a PNG image.' },
|
id: 'pdf-to-jpg',
|
||||||
{ id: 'pdf-to-webp', name: 'PDF to WebP', icon: 'file-image', subtitle: 'Convert each PDF page into a WebP image.' },
|
name: 'PDF to JPG',
|
||||||
{ id: 'pdf-to-bmp', name: 'PDF to BMP', icon: 'file-image', subtitle: 'Convert each PDF page into a BMP image.' },
|
icon: 'file-image',
|
||||||
{ id: 'pdf-to-tiff', name: 'PDF to TIFF', icon: 'file-image', subtitle: 'Convert each PDF page into a TIFF image.' },
|
subtitle: 'Convert each PDF page into a JPG image.',
|
||||||
{ id: 'pdf-to-greyscale', name: 'PDF to Greyscale', icon: 'palette', subtitle: 'Convert all colors to black and white.' },
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-png',
|
||||||
|
name: 'PDF to PNG',
|
||||||
|
icon: 'file-image',
|
||||||
|
subtitle: 'Convert each PDF page into a PNG image.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-webp',
|
||||||
|
name: 'PDF to WebP',
|
||||||
|
icon: 'file-image',
|
||||||
|
subtitle: 'Convert each PDF page into a WebP image.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-bmp',
|
||||||
|
name: 'PDF to BMP',
|
||||||
|
icon: 'file-image',
|
||||||
|
subtitle: 'Convert each PDF page into a BMP image.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-tiff',
|
||||||
|
name: 'PDF to TIFF',
|
||||||
|
icon: 'file-image',
|
||||||
|
subtitle: 'Convert each PDF page into a TIFF image.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-greyscale',
|
||||||
|
name: 'PDF to Greyscale',
|
||||||
|
icon: 'palette',
|
||||||
|
subtitle: 'Convert all colors to black and white.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/src/pages/pdf-to-json.html',
|
||||||
|
name: 'PDF to JSON',
|
||||||
|
icon: 'file-code',
|
||||||
|
subtitle: 'Convert PDF files to JSON format.',
|
||||||
|
},
|
||||||
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
|
// { id: 'pdf-to-markdown', name: 'PDF to Markdown', icon: 'file-pen', subtitle: 'Extract text into a Markdown file.' },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Organize & Manage',
|
name: 'Organize & Manage',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'ocr-pdf', name: 'OCR PDF', icon: 'scan-text', subtitle: 'Make a PDF searchable and copyable.' },
|
{
|
||||||
{ id: 'merge', name: 'Merge PDF', icon: 'combine', subtitle: 'Combine multiple PDFs into one file.' },
|
id: 'ocr-pdf',
|
||||||
{ id: 'organize', name: 'Organize PDF', icon: 'grip', subtitle: 'Reorder pages by dragging and dropping.' },
|
name: 'OCR PDF',
|
||||||
{ id: 'duplicate-organize', name: 'Duplicate & Organize', icon: 'files', subtitle: 'Duplicate, reorder, and delete pages.' },
|
icon: 'scan-text',
|
||||||
{ id: 'split', name: 'Split PDF', icon: 'scissors', subtitle: 'Extract a range of pages into a new PDF.' },
|
subtitle: 'Make a PDF searchable and copyable.',
|
||||||
{ id: 'split-in-half', name: 'Divide Pages', icon: 'table-columns-split', subtitle: 'Divide pages horizontally or vertically.' },
|
},
|
||||||
{ id: 'extract-pages', name: 'Extract Pages', icon: 'ungroup', subtitle: 'Save a selection of pages as new files.' },
|
{
|
||||||
{ id: 'delete-pages', name: 'Delete Pages', icon: 'trash-2', subtitle: 'Remove specific pages from your document.' },
|
id: 'merge',
|
||||||
{ id: 'add-blank-page', name: 'Add Blank Page', icon: 'file-plus-2', subtitle: 'Insert an empty page anywhere in your PDF.' },
|
name: 'Merge PDF',
|
||||||
{ id: 'reverse-pages', name: 'Reverse Pages', icon: 'arrow-down-z-a', subtitle: 'Flip the order of all pages in your document.' },
|
icon: 'combine',
|
||||||
{ id: 'rotate', name: 'Rotate PDF', icon: 'rotate-cw', subtitle: 'Turn pages in 90-degree increments.' },
|
subtitle: 'Combine multiple PDFs into one file.',
|
||||||
{ id: 'n-up', name: 'N-Up PDF', icon: 'layout-grid', subtitle: 'Arrange multiple pages onto a single sheet.' },
|
},
|
||||||
{ id: 'combine-single-page', name: 'Combine to Single Page', icon: 'unfold-vertical', subtitle: 'Stitch all pages into one continuous scroll.' },
|
{
|
||||||
{ id: 'view-metadata', name: 'View Metadata', icon: 'info', subtitle: 'Inspect the hidden properties of your PDF.' },
|
id: 'alternate-merge',
|
||||||
{ id: 'edit-metadata', name: 'Edit Metadata', icon: 'file-cog', subtitle: 'Change the author, title, and other properties.' },
|
name: 'Alternate & Mix Pages',
|
||||||
{ id: 'pdf-to-zip', name: 'PDFs to ZIP', icon: 'stretch-horizontal', subtitle: 'Package multiple PDF files into a ZIP archive.' },
|
icon: 'shuffle',
|
||||||
{ id: 'compare-pdfs', name: 'Compare PDFs', icon: 'git-compare', subtitle: 'Compare two PDFs side by side.' },
|
subtitle: 'Combine PDFs by alternating pages from each.',
|
||||||
{ id: 'posterize', name: 'Posterize PDF', icon: 'layout-grid', subtitle: 'Split a large page into multiple smaller pages.' },
|
},
|
||||||
]
|
{
|
||||||
|
id: 'organize',
|
||||||
|
name: 'Organize PDF',
|
||||||
|
icon: 'grip',
|
||||||
|
subtitle: 'Reorder pages by dragging and dropping.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'duplicate-organize',
|
||||||
|
name: 'Duplicate & Organize',
|
||||||
|
icon: 'files',
|
||||||
|
subtitle: 'Duplicate, reorder, and delete pages.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-attachments',
|
||||||
|
name: 'Add Attachments',
|
||||||
|
icon: 'paperclip',
|
||||||
|
subtitle: 'Embed one or more files into your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split',
|
||||||
|
name: 'Split PDF',
|
||||||
|
icon: 'scissors',
|
||||||
|
subtitle: 'Extract a range of pages into a new PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'split-in-half',
|
||||||
|
name: 'Divide Pages',
|
||||||
|
icon: 'table-columns-split',
|
||||||
|
subtitle: 'Divide pages horizontally or vertically.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'extract-pages',
|
||||||
|
name: 'Extract Pages',
|
||||||
|
icon: 'ungroup',
|
||||||
|
subtitle: 'Save a selection of pages as new files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delete-pages',
|
||||||
|
name: 'Delete Pages',
|
||||||
|
icon: 'trash-2',
|
||||||
|
subtitle: 'Remove specific pages from your document.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'add-blank-page',
|
||||||
|
name: 'Add Blank Page',
|
||||||
|
icon: 'file-plus-2',
|
||||||
|
subtitle: 'Insert an empty page anywhere in your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reverse-pages',
|
||||||
|
name: 'Reverse Pages',
|
||||||
|
icon: 'arrow-down-z-a',
|
||||||
|
subtitle: 'Flip the order of all pages in your document.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rotate',
|
||||||
|
name: 'Rotate PDF',
|
||||||
|
icon: 'rotate-cw',
|
||||||
|
subtitle: 'Turn pages in 90-degree increments.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'n-up',
|
||||||
|
name: 'N-Up PDF',
|
||||||
|
icon: 'layout-grid',
|
||||||
|
subtitle: 'Arrange multiple pages onto a single sheet.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'combine-single-page',
|
||||||
|
name: 'Combine to Single Page',
|
||||||
|
icon: 'unfold-vertical',
|
||||||
|
subtitle: 'Stitch all pages into one continuous scroll.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'view-metadata',
|
||||||
|
name: 'View Metadata',
|
||||||
|
icon: 'info',
|
||||||
|
subtitle: 'Inspect the hidden properties of your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit-metadata',
|
||||||
|
name: 'Edit Metadata',
|
||||||
|
icon: 'file-cog',
|
||||||
|
subtitle: 'Change the author, title, and other properties.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pdf-to-zip',
|
||||||
|
name: 'PDFs to ZIP',
|
||||||
|
icon: 'stretch-horizontal',
|
||||||
|
subtitle: 'Package multiple PDF files into a ZIP archive.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compare-pdfs',
|
||||||
|
name: 'Compare PDFs',
|
||||||
|
icon: 'git-compare',
|
||||||
|
subtitle: 'Compare two PDFs side by side.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'posterize',
|
||||||
|
name: 'Posterize PDF',
|
||||||
|
icon: 'notepad-text-dashed',
|
||||||
|
subtitle: 'Split a large page into multiple smaller pages.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Optimize & Repair',
|
name: 'Optimize & Repair',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'compress', name: 'Compress PDF', icon: 'zap', subtitle: 'Reduce the file size of your PDF.' },
|
{
|
||||||
{ id: 'fix-dimensions', name: 'Fix Page Size', icon: 'ruler-dimension-line', subtitle: 'Standardize all pages to a uniform size.' },
|
id: 'compress',
|
||||||
{ id: 'page-dimensions', name: 'Page Dimensions', icon: 'ruler', subtitle: 'Analyze page size, orientation, and units.' },
|
name: 'Compress PDF',
|
||||||
]
|
icon: 'zap',
|
||||||
|
subtitle: 'Reduce the file size of your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fix-dimensions',
|
||||||
|
name: 'Fix Page Size',
|
||||||
|
icon: 'ruler-dimension-line',
|
||||||
|
subtitle: 'Standardize all pages to a uniform size.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'linearize',
|
||||||
|
name: 'Linearize PDF',
|
||||||
|
icon: 'gauge',
|
||||||
|
subtitle: 'Optimize PDF for fast web viewing.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'page-dimensions',
|
||||||
|
name: 'Page Dimensions',
|
||||||
|
icon: 'ruler',
|
||||||
|
subtitle: 'Analyze page size, orientation, and units.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-restrictions',
|
||||||
|
name: 'Remove Restrictions',
|
||||||
|
icon: 'unlink',
|
||||||
|
subtitle:
|
||||||
|
'Remove password protection and security restrictions associated with digitally signed PDF files.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Secure PDF',
|
name: 'Secure PDF',
|
||||||
tools: [
|
tools: [
|
||||||
{ id: 'encrypt', name: 'Encrypt PDF', icon: 'lock', subtitle: 'Add a password to protect your PDF.' },
|
{
|
||||||
{ id: 'decrypt', name: 'Decrypt PDF', icon: 'unlock', subtitle: 'Remove password protection from a PDF.' },
|
id: 'encrypt',
|
||||||
{ id: 'flatten', name: 'Flatten PDF', icon: 'layers', subtitle: 'Make form fields and annotations non-editable.' },
|
name: 'Encrypt PDF',
|
||||||
{ id: 'remove-metadata', name: 'Remove Metadata', icon: 'file-x', subtitle: 'Strip hidden data from your PDF.' },
|
icon: 'lock',
|
||||||
{ id: 'change-permissions', name: 'Change Permissions', icon: 'shield-check', subtitle: 'Set or change user permissions on a PDF.' },
|
subtitle: 'Lock your PDF by adding a password.',
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
id: 'sanitize-pdf',
|
||||||
|
name: 'Sanitize PDF',
|
||||||
|
icon: 'brush-cleaning',
|
||||||
|
subtitle: 'Remove metadata, annotations, scripts, and more.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'decrypt',
|
||||||
|
name: 'Decrypt PDF',
|
||||||
|
icon: 'unlock',
|
||||||
|
subtitle: 'Unlock PDF by removing password protection.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'flatten',
|
||||||
|
name: 'Flatten PDF',
|
||||||
|
icon: 'layers',
|
||||||
|
subtitle: 'Make form fields and annotations non-editable.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'remove-metadata',
|
||||||
|
name: 'Remove Metadata',
|
||||||
|
icon: 'file-x',
|
||||||
|
subtitle: 'Strip hidden data from your PDF.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'change-permissions',
|
||||||
|
name: 'Change Permissions',
|
||||||
|
icon: 'shield-check',
|
||||||
|
subtitle: 'Set or change user permissions on a PDF.',
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
+318
-64
@@ -1,13 +1,24 @@
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { showLoader, hideLoader, showAlert, renderPageThumbnails, renderFileDisplay, switchView } from '../ui.js';
|
import {
|
||||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
showLoader,
|
||||||
|
hideLoader,
|
||||||
|
showAlert,
|
||||||
|
renderPageThumbnails,
|
||||||
|
renderFileDisplay,
|
||||||
|
switchView,
|
||||||
|
} from '../ui.js';
|
||||||
|
import { formatIsoDate, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { setupCanvasEditor } from '../canvasEditor.js';
|
import { setupCanvasEditor } from '../canvasEditor.js';
|
||||||
import { toolLogic } from '../logic/index.js';
|
import { toolLogic } from '../logic/index.js';
|
||||||
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
|
import { renderDuplicateOrganizeThumbnails } from '../logic/duplicate-organize.js';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
import { icons, createIcons } from 'lucide';
|
import { icons, createIcons } from 'lucide';
|
||||||
import Sortable from 'sortablejs';
|
import Sortable from 'sortablejs';
|
||||||
import { multiFileTools, simpleTools, singlePdfLoadTools } from '../config/pdf-tools.js';
|
import {
|
||||||
|
multiFileTools,
|
||||||
|
simpleTools,
|
||||||
|
singlePdfLoadTools,
|
||||||
|
} from '../config/pdf-tools.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
async function handleSinglePdfUpload(toolId, file) {
|
async function handleSinglePdfUpload(toolId, file) {
|
||||||
@@ -15,17 +26,27 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
try {
|
try {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
state.pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||||
ignoreEncryption: true
|
ignoreEncryption: true,
|
||||||
});
|
});
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
if (state.pdfDoc.isEncrypted && toolId !== 'decrypt' && toolId !== 'change-permissions') {
|
if (
|
||||||
showAlert('Protected PDF', 'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.');
|
state.pdfDoc.isEncrypted &&
|
||||||
|
toolId !== 'decrypt' &&
|
||||||
|
toolId !== 'change-permissions' &&
|
||||||
|
toolId !== 'remove-restrictions'
|
||||||
|
) {
|
||||||
|
showAlert(
|
||||||
|
'Protected PDF',
|
||||||
|
'This PDF is password-protected. Please use the Decrypt or Change Permissions tool first.'
|
||||||
|
);
|
||||||
switchView('grid');
|
switchView('grid');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsDiv = document.querySelector('[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]');
|
const optionsDiv = document.querySelector(
|
||||||
|
'[id$="-options"], [id$="-preview"], [id$="-organizer"], [id$="-rotator"], [id$="-editor"]'
|
||||||
|
);
|
||||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
@@ -34,34 +55,52 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
processBtn.classList.remove('hidden');
|
processBtn.classList.remove('hidden');
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
const func =
|
||||||
|
typeof logic.process === 'function' ? logic.process : logic;
|
||||||
processBtn.onclick = func;
|
processBtn.onclick = func;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['split', 'delete-pages', 'add-blank-page', 'extract-pages', 'add-header-footer'].includes(toolId)) {
|
if (
|
||||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
|
[
|
||||||
|
'split',
|
||||||
|
'delete-pages',
|
||||||
|
'add-blank-page',
|
||||||
|
'extract-pages',
|
||||||
|
'add-header-footer',
|
||||||
|
].includes(toolId)
|
||||||
|
) {
|
||||||
|
document.getElementById('total-pages').textContent = state.pdfDoc
|
||||||
|
.getPageCount()
|
||||||
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'organize' || toolId === 'rotate') {
|
if (toolId === 'organize' || toolId === 'rotate') {
|
||||||
await renderPageThumbnails(toolId, state.pdfDoc);
|
await renderPageThumbnails(toolId, state.pdfDoc);
|
||||||
|
|
||||||
if (toolId === 'rotate') {
|
if (toolId === 'rotate') {
|
||||||
const rotateAllControls = document.getElementById('rotate-all-controls');
|
const rotateAllControls = document.getElementById(
|
||||||
|
'rotate-all-controls'
|
||||||
|
);
|
||||||
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
const rotateAllLeftBtn = document.getElementById('rotate-all-left-btn');
|
||||||
const rotateAllRightBtn = document.getElementById('rotate-all-right-btn');
|
const rotateAllRightBtn = document.getElementById(
|
||||||
|
'rotate-all-right-btn'
|
||||||
|
);
|
||||||
|
|
||||||
rotateAllControls.classList.remove('hidden');
|
rotateAllControls.classList.remove('hidden');
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
|
|
||||||
const rotateAll = (direction) => {
|
const rotateAll = (direction) => {
|
||||||
document.querySelectorAll('.page-rotator-item').forEach(item => {
|
document.querySelectorAll('.page-rotator-item').forEach((item) => {
|
||||||
const currentRotation = parseInt((item as HTMLElement).dataset.rotation || '0');
|
const currentRotation = parseInt(
|
||||||
const newRotation = (currentRotation + (direction * 90) + 360) % 360;
|
(item as HTMLElement).dataset.rotation || '0'
|
||||||
|
);
|
||||||
|
const newRotation = (currentRotation + direction * 90 + 360) % 360;
|
||||||
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
(item as HTMLElement).dataset.rotation = newRotation.toString();
|
||||||
const thumbnail = item.querySelector('canvas, img');
|
const thumbnail = item.querySelector('canvas, img');
|
||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
(thumbnail as HTMLElement).style.transform = `rotate(${newRotation}deg)`;
|
(thumbnail as HTMLElement).style.transform =
|
||||||
|
`rotate(${newRotation}deg)`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -83,13 +122,16 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
const pdfBytes = await readFileAsArrayBuffer(state.files[0]);
|
||||||
const pdfjsDoc = await pdfjsLib.getDocument({ data: pdfBytes as ArrayBuffer }).promise;
|
const pdfjsDoc = await pdfjsLib.getDocument({
|
||||||
const [metadata, fieldObjects] = await Promise.all([
|
data: pdfBytes as ArrayBuffer,
|
||||||
|
}).promise;
|
||||||
|
const [metadataResult, fieldObjects] = await Promise.all([
|
||||||
pdfjsDoc.getMetadata(),
|
pdfjsDoc.getMetadata(),
|
||||||
pdfjsDoc.getFieldObjects()
|
pdfjsDoc.getFieldObjects(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { info, metadata: rawXmpString } = metadata;
|
const { info, metadata } = metadataResult;
|
||||||
|
const rawXmpString = metadata ? metadata.getRaw() : null;
|
||||||
|
|
||||||
resultsDiv.textContent = ''; // Clear safely
|
resultsDiv.textContent = ''; // Clear safely
|
||||||
|
|
||||||
@@ -100,7 +142,8 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
h3.className = 'text-lg font-semibold text-white mb-2';
|
h3.className = 'text-lg font-semibold text-white mb-2';
|
||||||
h3.textContent = title;
|
h3.textContent = title;
|
||||||
const ul = document.createElement('ul');
|
const ul = document.createElement('ul');
|
||||||
ul.className = 'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
ul.className =
|
||||||
|
'space-y-3 text-sm bg-gray-900 p-4 rounded-lg border border-gray-700';
|
||||||
wrapper.append(h3, ul);
|
wrapper.append(h3, ul);
|
||||||
return { wrapper, ul };
|
return { wrapper, ul };
|
||||||
};
|
};
|
||||||
@@ -119,7 +162,12 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const parsePdfDate = (pdfDate) => {
|
const parsePdfDate = (pdfDate) => {
|
||||||
if (!pdfDate || typeof pdfDate !== 'string' || !pdfDate.startsWith('D:')) return pdfDate;
|
if (
|
||||||
|
!pdfDate ||
|
||||||
|
typeof pdfDate !== 'string' ||
|
||||||
|
!pdfDate.startsWith('D:')
|
||||||
|
)
|
||||||
|
return pdfDate;
|
||||||
try {
|
try {
|
||||||
const year = pdfDate.substring(2, 6);
|
const year = pdfDate.substring(2, 6);
|
||||||
const month = pdfDate.substring(6, 8);
|
const month = pdfDate.substring(6, 8);
|
||||||
@@ -127,18 +175,40 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const hour = pdfDate.substring(10, 12);
|
const hour = pdfDate.substring(10, 12);
|
||||||
const minute = pdfDate.substring(12, 14);
|
const minute = pdfDate.substring(12, 14);
|
||||||
const second = pdfDate.substring(14, 16);
|
const second = pdfDate.substring(14, 16);
|
||||||
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`).toLocaleString();
|
return new Date(
|
||||||
} catch { return pdfDate; }
|
`${year}-${month}-${day}T${hour}:${minute}:${second}Z`
|
||||||
|
).toLocaleString();
|
||||||
|
} catch {
|
||||||
|
return pdfDate;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const infoSection = createSection('Info Dictionary');
|
const infoSection = createSection('Info Dictionary');
|
||||||
if (info && Object.keys(info).length > 0) {
|
if (info && Object.keys(info).length > 0) {
|
||||||
for (const key in info) {
|
for (const key in info) {
|
||||||
let value = info[key] || '- Not Set -';
|
let value = info[key];
|
||||||
if ((key === 'CreationDate' || key === 'ModDate') && typeof value === 'string') {
|
let displayValue;
|
||||||
value = parsePdfDate(value);
|
|
||||||
|
if (value === null || typeof value === 'undefined') {
|
||||||
|
displayValue = '- Not Set -';
|
||||||
|
} else if (typeof value === 'object' && value.name) {
|
||||||
|
displayValue = value.name;
|
||||||
|
} else if (typeof value === 'object') {
|
||||||
|
try {
|
||||||
|
displayValue = JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
displayValue = '[object Object]';
|
||||||
}
|
}
|
||||||
infoSection.ul.appendChild(createListItem(key, String(value)));
|
} else if (
|
||||||
|
(key === 'CreationDate' || key === 'ModDate') &&
|
||||||
|
typeof value === 'string'
|
||||||
|
) {
|
||||||
|
displayValue = parsePdfDate(value);
|
||||||
|
} else {
|
||||||
|
displayValue = String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
infoSection.ul.appendChild(createListItem(key, displayValue));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
infoSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No Info Dictionary data found -</span></li>`;
|
||||||
@@ -150,32 +220,137 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
for (const fieldName in fieldObjects) {
|
for (const fieldName in fieldObjects) {
|
||||||
const field = fieldObjects[fieldName][0];
|
const field = fieldObjects[fieldName][0];
|
||||||
const value = (field as any).fieldValue || '- Not Set -';
|
const value = (field as any).fieldValue || '- Not Set -';
|
||||||
fieldsSection.ul.appendChild(createListItem(fieldName, String(value)));
|
fieldsSection.ul.appendChild(
|
||||||
|
createListItem(fieldName, String(value))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
fieldsSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No interactive form fields found -</span></li>`;
|
||||||
}
|
}
|
||||||
resultsDiv.appendChild(fieldsSection.wrapper);
|
resultsDiv.appendChild(fieldsSection.wrapper);
|
||||||
|
|
||||||
const xmpSection = createSection('XMP Metadata (Raw XML)');
|
const createXmpListItem = (key, value, indent = 0) => {
|
||||||
const xmpContainer = document.createElement('div');
|
const li = document.createElement('li');
|
||||||
xmpContainer.className = 'bg-gray-900 p-4 rounded-lg border border-gray-700';
|
li.className = 'flex flex-col sm:flex-row';
|
||||||
|
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.className = 'w-56 flex-shrink-0 text-gray-400';
|
||||||
|
strong.textContent = key;
|
||||||
|
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex-grow text-white break-all';
|
||||||
|
div.textContent = value;
|
||||||
|
|
||||||
|
li.append(strong, div);
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createXmpHeaderItem = (key, indent = 0) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'flex pt-2';
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.className = 'w-full flex-shrink-0 text-gray-300 font-medium';
|
||||||
|
strong.textContent = key;
|
||||||
|
strong.style.paddingLeft = `${indent * 1.2}rem`;
|
||||||
|
li.append(strong);
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendXmpNodes = (xmlNode, ulElement, indentLevel) => {
|
||||||
|
const xmpDateKeys = [
|
||||||
|
'xap:CreateDate',
|
||||||
|
'xap:ModifyDate',
|
||||||
|
'xap:MetadataDate',
|
||||||
|
];
|
||||||
|
|
||||||
|
const childNodes = Array.from(xmlNode.children);
|
||||||
|
|
||||||
|
for (const child of childNodes) {
|
||||||
|
if ((child as Element).nodeType !== 1) continue;
|
||||||
|
|
||||||
|
let key = (child as Element).tagName;
|
||||||
|
const elementChildren = Array.from(
|
||||||
|
(child as Element).children
|
||||||
|
).filter((c) => c.nodeType === 1);
|
||||||
|
|
||||||
|
if (key === 'rdf:li') {
|
||||||
|
appendXmpNodes(child, ulElement, indentLevel);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === 'rdf:Alt') {
|
||||||
|
key = '(alt container)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(child as Element).getAttribute('rdf:parseType') === 'Resource' &&
|
||||||
|
elementChildren.length === 0
|
||||||
|
) {
|
||||||
|
ulElement.appendChild(
|
||||||
|
createXmpListItem(key, '(Empty Resource)', indentLevel)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementChildren.length > 0) {
|
||||||
|
ulElement.appendChild(createXmpHeaderItem(key, indentLevel));
|
||||||
|
appendXmpNodes(child, ulElement, indentLevel + 1);
|
||||||
|
} else {
|
||||||
|
let value = (child as Element).textContent.trim();
|
||||||
|
if (value) {
|
||||||
|
if (xmpDateKeys.includes(key)) {
|
||||||
|
value = formatIsoDate(value);
|
||||||
|
}
|
||||||
|
ulElement.appendChild(
|
||||||
|
createXmpListItem(key, value, indentLevel)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const xmpSection = createSection('XMP Metadata');
|
||||||
if (rawXmpString) {
|
if (rawXmpString) {
|
||||||
|
try {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(
|
||||||
|
rawXmpString,
|
||||||
|
'application/xml'
|
||||||
|
);
|
||||||
|
|
||||||
|
const descriptions = xmlDoc.getElementsByTagName('rdf:Description');
|
||||||
|
if (descriptions.length > 0) {
|
||||||
|
for (const desc of descriptions) {
|
||||||
|
appendXmpNodes(desc, xmpSection.ul, 0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appendXmpNodes(xmlDoc.documentElement, xmpSection.ul, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xmpSection.ul.children.length === 0) {
|
||||||
|
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No parseable XMP properties found -</span></li>`;
|
||||||
|
}
|
||||||
|
} catch (xmlError) {
|
||||||
|
console.error('Failed to parse XMP XML:', xmlError);
|
||||||
|
xmpSection.ul.innerHTML = `<li><span class="text-red-500 italic">- Error parsing XMP XML. Displaying raw. -</span></li>`;
|
||||||
const pre = document.createElement('pre');
|
const pre = document.createElement('pre');
|
||||||
pre.className = 'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
pre.className =
|
||||||
pre.textContent = String(rawXmpString);
|
'text-xs text-gray-300 whitespace-pre-wrap break-all';
|
||||||
xmpContainer.appendChild(pre);
|
pre.textContent = rawXmpString;
|
||||||
|
xmpSection.ul.appendChild(pre);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
xmpContainer.innerHTML = `<p class="text-gray-500 italic">- No XMP metadata found -</p>`;
|
xmpSection.ul.innerHTML = `<li><span class="text-gray-500 italic">- No XMP metadata found -</span></li>`;
|
||||||
}
|
}
|
||||||
xmpSection.wrapper.appendChild(xmpContainer);
|
|
||||||
resultsDiv.appendChild(xmpSection.wrapper);
|
resultsDiv.appendChild(xmpSection.wrapper);
|
||||||
|
|
||||||
resultsDiv.classList.remove('hidden');
|
resultsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to view metadata or fields:", e);
|
console.error('Failed to view metadata or fields:', e);
|
||||||
showAlert('Error', 'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not fully analyze the PDF. It may be corrupted or have an unusual structure.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
@@ -192,14 +367,23 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
(document.getElementById('meta-title') as HTMLInputElement).value = state.pdfDoc.getTitle() || '';
|
(document.getElementById('meta-title') as HTMLInputElement).value =
|
||||||
(document.getElementById('meta-author') as HTMLInputElement).value = state.pdfDoc.getAuthor() || '';
|
state.pdfDoc.getTitle() || '';
|
||||||
(document.getElementById('meta-subject') as HTMLInputElement).value = state.pdfDoc.getSubject() || '';
|
(document.getElementById('meta-author') as HTMLInputElement).value =
|
||||||
(document.getElementById('meta-keywords') as HTMLInputElement).value = state.pdfDoc.getKeywords() || '';
|
state.pdfDoc.getAuthor() || '';
|
||||||
(document.getElementById('meta-creator') as HTMLInputElement).value = state.pdfDoc.getCreator() || '';
|
(document.getElementById('meta-subject') as HTMLInputElement).value =
|
||||||
(document.getElementById('meta-producer') as HTMLInputElement).value = state.pdfDoc.getProducer() || '';
|
state.pdfDoc.getSubject() || '';
|
||||||
(document.getElementById('meta-creation-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getCreationDate());
|
(document.getElementById('meta-keywords') as HTMLInputElement).value =
|
||||||
(document.getElementById('meta-mod-date') as HTMLInputElement).value = formatDateForInput(state.pdfDoc.getModificationDate());
|
state.pdfDoc.getKeywords() || '';
|
||||||
|
(document.getElementById('meta-creator') as HTMLInputElement).value =
|
||||||
|
state.pdfDoc.getCreator() || '';
|
||||||
|
(document.getElementById('meta-producer') as HTMLInputElement).value =
|
||||||
|
state.pdfDoc.getProducer() || '';
|
||||||
|
(
|
||||||
|
document.getElementById('meta-creation-date') as HTMLInputElement
|
||||||
|
).value = formatDateForInput(state.pdfDoc.getCreationDate());
|
||||||
|
(document.getElementById('meta-mod-date') as HTMLInputElement).value =
|
||||||
|
formatDateForInput(state.pdfDoc.getModificationDate());
|
||||||
|
|
||||||
addBtn.onclick = () => {
|
addBtn.onclick = () => {
|
||||||
const fieldWrapper = document.createElement('div');
|
const fieldWrapper = document.createElement('div');
|
||||||
@@ -208,16 +392,19 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
const keyInput = document.createElement('input');
|
const keyInput = document.createElement('input');
|
||||||
keyInput.type = 'text';
|
keyInput.type = 'text';
|
||||||
keyInput.placeholder = 'Key (e.g., Department)';
|
keyInput.placeholder = 'Key (e.g., Department)';
|
||||||
keyInput.className = 'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
keyInput.className =
|
||||||
|
'custom-meta-key w-1/3 bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||||
|
|
||||||
const valueInput = document.createElement('input');
|
const valueInput = document.createElement('input');
|
||||||
valueInput.type = 'text';
|
valueInput.type = 'text';
|
||||||
valueInput.placeholder = 'Value (e.g., Marketing)';
|
valueInput.placeholder = 'Value (e.g., Marketing)';
|
||||||
valueInput.className = 'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
valueInput.className =
|
||||||
|
'custom-meta-value flex-grow bg-gray-800 border border-gray-600 text-white rounded-lg p-2';
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.type = 'button';
|
removeBtn.type = 'button';
|
||||||
removeBtn.className = 'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
|
removeBtn.className =
|
||||||
|
'btn p-2 text-red-500 hover:bg-gray-700 rounded-full';
|
||||||
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
removeBtn.innerHTML = '<i data-lucide="trash-2"></i>';
|
||||||
removeBtn.addEventListener('click', () => fieldWrapper.remove());
|
removeBtn.addEventListener('click', () => fieldWrapper.remove());
|
||||||
|
|
||||||
@@ -231,7 +418,9 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'cropper') {
|
if (toolId === 'cropper') {
|
||||||
document.getElementById('cropper-ui-container').classList.remove('hidden');
|
document
|
||||||
|
.getElementById('cropper-ui-container')
|
||||||
|
.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toolId === 'page-dimensions') {
|
if (toolId === 'page-dimensions') {
|
||||||
@@ -243,12 +432,62 @@ async function handleSinglePdfUpload(toolId, file) {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'Could not load PDF. The file may be invalid, corrupted, or password-protected.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not load PDF. The file may be invalid, corrupted, or password-protected.'
|
||||||
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMultiFileUpload(toolId) {
|
async function handleMultiFileUpload(toolId) {
|
||||||
|
if (
|
||||||
|
toolId === 'merge' ||
|
||||||
|
toolId === 'alternate-merge' ||
|
||||||
|
toolId === 'reverse-pages'
|
||||||
|
) {
|
||||||
|
const pdfFilesUnloaded: File[] = [];
|
||||||
|
|
||||||
|
state.files.forEach((file) => {
|
||||||
|
if (file.type === 'application/pdf') {
|
||||||
|
pdfFilesUnloaded.push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfFilesLoaded = await Promise.all(
|
||||||
|
pdfFilesUnloaded.map(async (file) => {
|
||||||
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
|
const pdfDoc = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
||||||
|
ignoreEncryption: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
pdfDoc,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundEncryptedPDFs = pdfFilesLoaded.filter(
|
||||||
|
(pdf) => pdf.pdfDoc.isEncrypted
|
||||||
|
);
|
||||||
|
|
||||||
|
if (foundEncryptedPDFs.length > 0) {
|
||||||
|
const encryptedPDFFileNames = [];
|
||||||
|
foundEncryptedPDFs.forEach((encryptedPDF) => {
|
||||||
|
encryptedPDFFileNames.push(encryptedPDF.file.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorMessage = `PDFs found that are password-protected\n\nPlease use the Decrypt or Change Permissions tool on these files first:\n\n${encryptedPDFFileNames.join('\n')}`;
|
||||||
|
|
||||||
|
showAlert('Protected PDFs', errorMessage);
|
||||||
|
|
||||||
|
switchView('grid');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
if (processBtn) {
|
if (processBtn) {
|
||||||
(processBtn as HTMLButtonElement).disabled = false;
|
(processBtn as HTMLButtonElement).disabled = false;
|
||||||
@@ -261,22 +500,26 @@ function handleMultiFileUpload(toolId) {
|
|||||||
|
|
||||||
if (toolId === 'merge') {
|
if (toolId === 'merge') {
|
||||||
toolLogic.merge.setup();
|
toolLogic.merge.setup();
|
||||||
|
} else if (toolId === 'alternate-merge') {
|
||||||
|
toolLogic['alternate-merge'].setup();
|
||||||
} else if (toolId === 'image-to-pdf') {
|
} else if (toolId === 'image-to-pdf') {
|
||||||
const imageList = document.getElementById('image-list');
|
const imageList = document.getElementById('image-list');
|
||||||
imageList.textContent = ''; // Clear safely
|
imageList.textContent = ''; // Clear safely
|
||||||
|
|
||||||
state.files.forEach(file => {
|
state.files.forEach((file) => {
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = "relative group cursor-move";
|
li.className = 'relative group cursor-move';
|
||||||
li.dataset.fileName = file.name;
|
li.dataset.fileName = file.name;
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = url;
|
img.src = url;
|
||||||
img.className = "w-full h-full object-cover rounded-md border-2 border-gray-600";
|
img.className =
|
||||||
|
'w-full h-full object-cover rounded-md border-2 border-gray-600';
|
||||||
|
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.className = "absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1";
|
p.className =
|
||||||
|
'absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs text-center truncate p-1';
|
||||||
p.textContent = file.name; // Safe insertion
|
p.textContent = file.name; // Safe insertion
|
||||||
|
|
||||||
li.append(img, p);
|
li.append(img, p);
|
||||||
@@ -314,11 +557,14 @@ export function setupFileInputHandler(toolId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMultiFileTool) {
|
if (isMultiFileTool) {
|
||||||
handleMultiFileUpload(toolId);
|
await handleMultiFileUpload(toolId);
|
||||||
} else if (singlePdfLoadTools.includes(toolId)) {
|
} else if (singlePdfLoadTools.includes(toolId)) {
|
||||||
await handleSinglePdfUpload(toolId, state.files[0]);
|
await handleSinglePdfUpload(toolId, state.files[0]);
|
||||||
} else if (simpleTools.includes(toolId)) {
|
} else if (simpleTools.includes(toolId)) {
|
||||||
const optionsDivId = toolId === 'change-permissions' ? 'permissions-options' : `${toolId}-options`;
|
const optionsDivId =
|
||||||
|
toolId === 'change-permissions'
|
||||||
|
? 'permissions-options'
|
||||||
|
: `${toolId}-options`;
|
||||||
const optionsDiv = document.getElementById(optionsDivId);
|
const optionsDiv = document.getElementById(optionsDivId);
|
||||||
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
if (optionsDiv) optionsDiv.classList.remove('hidden');
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
@@ -327,7 +573,8 @@ export function setupFileInputHandler(toolId) {
|
|||||||
processBtn.onclick = () => {
|
processBtn.onclick = () => {
|
||||||
const logic = toolLogic[toolId];
|
const logic = toolLogic[toolId];
|
||||||
if (logic) {
|
if (logic) {
|
||||||
const func = typeof logic.process === 'function' ? logic.process : logic;
|
const func =
|
||||||
|
typeof logic.process === 'function' ? logic.process : logic;
|
||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -371,7 +618,9 @@ export function setupFileInputHandler(toolId) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fileInput.addEventListener('change', (e) => processFiles(Array.from((e.target as HTMLInputElement).files || [])));
|
fileInput.addEventListener('change', (e) =>
|
||||||
|
processFiles(Array.from((e.target as HTMLInputElement).files || []))
|
||||||
|
);
|
||||||
|
|
||||||
const setupAddMoreButton = () => {
|
const setupAddMoreButton = () => {
|
||||||
const addMoreBtn = document.getElementById('add-more-btn');
|
const addMoreBtn = document.getElementById('add-more-btn');
|
||||||
@@ -394,8 +643,13 @@ export function setupFileInputHandler(toolId) {
|
|||||||
const fileControls = document.getElementById('file-controls');
|
const fileControls = document.getElementById('file-controls');
|
||||||
if (fileControls) fileControls.classList.add('hidden');
|
if (fileControls) fileControls.classList.add('hidden');
|
||||||
|
|
||||||
const toolSpecificUI = ['file-list', 'page-merge-preview', 'image-list'];
|
const toolSpecificUI = [
|
||||||
toolSpecificUI.forEach(id => {
|
'file-list',
|
||||||
|
'page-merge-preview',
|
||||||
|
'image-list',
|
||||||
|
'alternate-file-list',
|
||||||
|
];
|
||||||
|
toolSpecificUI.forEach((id) => {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.textContent = '';
|
if (el) el.textContent = '';
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function setupToolInterface(toolId: any) {
|
|||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
behavior: 'instant' as ScrollBehavior
|
behavior: 'instant' as ScrollBehavior,
|
||||||
});
|
});
|
||||||
|
|
||||||
state.activeTool = toolId;
|
state.activeTool = toolId;
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||||
|
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
|
||||||
|
import { state } from '../state';
|
||||||
|
let attachments: File[] = [];
|
||||||
|
|
||||||
|
export async function addAttachments() {
|
||||||
|
if (!state.pdfDoc) {
|
||||||
|
showAlert('Error', 'Main PDF is not loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attachments.length === 0) {
|
||||||
|
showAlert('No Files', 'Please select at least one file to attach.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Embedding files into PDF...');
|
||||||
|
try {
|
||||||
|
const pdfDoc = state.pdfDoc;
|
||||||
|
|
||||||
|
for (let i = 0; i < attachments.length; i++) {
|
||||||
|
const file = attachments[i];
|
||||||
|
showLoader(`Attaching ${file.name} (${i + 1}/${attachments.length})...`);
|
||||||
|
|
||||||
|
const fileBytes = await readFileAsArrayBuffer(file);
|
||||||
|
|
||||||
|
await pdfDoc.attach(fileBytes as ArrayBuffer, file.name, {
|
||||||
|
mimeType: file.type || 'application/octet-stream',
|
||||||
|
description: `Attached file: ${file.name}`,
|
||||||
|
creationDate: new Date(),
|
||||||
|
modificationDate: new Date(file.lastModified),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([pdfBytes], { type: 'application/pdf' }),
|
||||||
|
`attached-${state.files[0].name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
`${attachments.length} file(s) attached successfully.`
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error attaching files:', error);
|
||||||
|
showAlert('Error', `Failed to attach files: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
clearAttachments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAttachments() {
|
||||||
|
attachments = [];
|
||||||
|
const fileListDiv = document.getElementById('attachment-file-list');
|
||||||
|
const attachmentInput = document.getElementById(
|
||||||
|
'attachment-files-input'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
|
if (fileListDiv) fileListDiv.innerHTML = '';
|
||||||
|
if (attachmentInput) attachmentInput.value = '';
|
||||||
|
if (processBtn) {
|
||||||
|
processBtn.disabled = true;
|
||||||
|
processBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isSetup = false; // Prevent duplicate setup
|
||||||
|
|
||||||
|
export function setupAddAttachmentsTool() {
|
||||||
|
if (isSetup) return; // Already set up
|
||||||
|
isSetup = true;
|
||||||
|
|
||||||
|
const optionsDiv = document.getElementById('attachment-options');
|
||||||
|
const attachmentInput = document.getElementById(
|
||||||
|
'attachment-files-input'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const fileListDiv = document.getElementById('attachment-file-list');
|
||||||
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
|
if (!optionsDiv || !attachmentInput || !fileListDiv || !processBtn) {
|
||||||
|
console.error('Attachment tool UI elements not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
optionsDiv.classList.remove('hidden');
|
||||||
|
|
||||||
|
attachmentInput.addEventListener('change', (e) => {
|
||||||
|
const files = (e.target as HTMLInputElement).files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
attachments = Array.from(files);
|
||||||
|
|
||||||
|
fileListDiv.innerHTML = '';
|
||||||
|
attachments.forEach((file) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className =
|
||||||
|
'flex justify-between items-center p-2 bg-gray-800 rounded-md text-white';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('span');
|
||||||
|
nameSpan.className = 'truncate text-sm';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
|
const sizeSpan = document.createElement('span');
|
||||||
|
sizeSpan.className = 'text-xs text-gray-400';
|
||||||
|
sizeSpan.textContent = `${Math.round(file.size / 1024)} KB`;
|
||||||
|
|
||||||
|
div.appendChild(nameSpan);
|
||||||
|
div.appendChild(sizeSpan);
|
||||||
|
fileListDiv.appendChild(div);
|
||||||
|
});
|
||||||
|
|
||||||
|
processBtn.disabled = false;
|
||||||
|
processBtn.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
clearAttachments();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
processBtn.onclick = addAttachments;
|
||||||
|
}
|
||||||
@@ -6,19 +6,39 @@ import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
|||||||
export async function addBlankPage() {
|
export async function addBlankPage() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const pageNumberInput = document.getElementById('page-number').value;
|
const pageNumberInput = document.getElementById('page-number').value;
|
||||||
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
const pageCountInput = document.getElementById('page-count').value;
|
||||||
|
|
||||||
if (pageNumberInput.trim() === '') {
|
if (pageNumberInput.trim() === '') {
|
||||||
showAlert('Invalid Input', 'Please enter a page number.');
|
showAlert('Invalid Input', 'Please enter a page number.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const position = parseInt(pageNumberInput);
|
if (pageCountInput.trim() === '') {
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
showAlert('Invalid Input', 'Please enter the number of pages to insert.');
|
||||||
if (isNaN(position) || position < 0 || position > totalPages) {
|
|
||||||
showAlert('Invalid Input', `Please enter a number between 0 and ${totalPages}.`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Adding page...');
|
const position = parseInt(pageNumberInput);
|
||||||
|
const pageCount = parseInt(pageCountInput);
|
||||||
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
|
if (isNaN(position) || position < 0 || position > totalPages) {
|
||||||
|
showAlert(
|
||||||
|
'Invalid Input',
|
||||||
|
`Please enter a number between 0 and ${totalPages}.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(pageCount) || pageCount < 1) {
|
||||||
|
showAlert(
|
||||||
|
'Invalid Input',
|
||||||
|
'Please enter a valid number of pages (1 or more).'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader(`Adding ${pageCount} blank page${pageCount > 1 ? 's' : ''}...`);
|
||||||
try {
|
try {
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
const { width, height } = state.pdfDoc.getPage(0).getSize();
|
||||||
@@ -32,7 +52,10 @@ export async function addBlankPage() {
|
|||||||
copied.forEach((p: any) => newPdf.addPage(p));
|
copied.forEach((p: any) => newPdf.addPage(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the specified number of blank pages
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
newPdf.addPage([width, height]);
|
newPdf.addPage([width, height]);
|
||||||
|
}
|
||||||
|
|
||||||
if (indicesAfter.length > 0) {
|
if (indicesAfter.length > 0) {
|
||||||
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
const copied = await newPdf.copyPages(state.pdfDoc, indicesAfter);
|
||||||
@@ -40,11 +63,13 @@ export async function addBlankPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'page-added.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
`blank-page${pageCount > 1 ? 's' : ''}-added.pdf`
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not add a blank page.');
|
showAlert('Error', `Could not add blank page${pageCount > 1 ? 's' : ''}.`);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { state } from '../state.js';
|
|||||||
|
|
||||||
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||||
|
|
||||||
|
|
||||||
export function setupHeaderFooterUI() {
|
export function setupHeaderFooterUI() {
|
||||||
const totalPagesSpan = document.getElementById('total-pages');
|
const totalPagesSpan = document.getElementById('total-pages');
|
||||||
if (totalPagesSpan && state.pdfDoc) {
|
if (totalPagesSpan && state.pdfDoc) {
|
||||||
@@ -48,14 +47,16 @@ export async function addHeaderFooter() {
|
|||||||
// --- 3. Parse page range to determine which pages to modify ---
|
// --- 3. Parse page range to determine which pages to modify ---
|
||||||
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
const indicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||||
if (indicesToProcess.length === 0) {
|
if (indicesToProcess.length === 0) {
|
||||||
throw new Error("Invalid page range specified. Please check your input (e.g., '1-3, 5').");
|
throw new Error(
|
||||||
|
"Invalid page range specified. Please check your input (e.g., '1-3, 5')."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 4. Define drawing options with new values ---
|
// --- 4. Define drawing options with new values ---
|
||||||
const drawOptions = {
|
const drawOptions = {
|
||||||
font: helveticaFont,
|
font: helveticaFont,
|
||||||
size: fontSize,
|
size: fontSize,
|
||||||
color: rgb(fontColor.r, fontColor.g, fontColor.b)
|
color: rgb(fontColor.r, fontColor.g, fontColor.b),
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 5. Loop over only the selected pages ---
|
// --- 5. Loop over only the selected pages ---
|
||||||
@@ -67,9 +68,8 @@ export async function addHeaderFooter() {
|
|||||||
const pageNumber = pageIndex + 1; // For dynamic text
|
const pageNumber = pageIndex + 1; // For dynamic text
|
||||||
|
|
||||||
// Helper to replace placeholders like {page} and {total}
|
// Helper to replace placeholders like {page} and {total}
|
||||||
const processText = (text: any) => text
|
const processText = (text: any) =>
|
||||||
.replace(/{page}/g, pageNumber)
|
text.replace(/{page}/g, pageNumber).replace(/{total}/g, totalPages);
|
||||||
.replace(/{total}/g, totalPages);
|
|
||||||
|
|
||||||
// Get processed text for the current page
|
// Get processed text for the current page
|
||||||
const processedTexts = {
|
const processedTexts = {
|
||||||
@@ -81,17 +81,73 @@ export async function addHeaderFooter() {
|
|||||||
footerRight: processText(texts.footerRight),
|
footerRight: processText(texts.footerRight),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (processedTexts.headerLeft) page.drawText(processedTexts.headerLeft, { ...drawOptions, x: margin, y: height - margin });
|
if (processedTexts.headerLeft)
|
||||||
if (processedTexts.headerCenter) page.drawText(processedTexts.headerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.headerCenter, fontSize) / 2, y: height - margin });
|
page.drawText(processedTexts.headerLeft, {
|
||||||
if (processedTexts.headerRight) page.drawText(processedTexts.headerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.headerRight, fontSize), y: height - margin });
|
...drawOptions,
|
||||||
if (processedTexts.footerLeft) page.drawText(processedTexts.footerLeft, { ...drawOptions, x: margin, y: margin });
|
x: margin,
|
||||||
if (processedTexts.footerCenter) page.drawText(processedTexts.footerCenter, { ...drawOptions, x: (width / 2) - helveticaFont.widthOfTextAtSize(processedTexts.footerCenter, fontSize) / 2, y: margin });
|
y: height - margin,
|
||||||
if (processedTexts.footerRight) page.drawText(processedTexts.footerRight, { ...drawOptions, x: width - margin - helveticaFont.widthOfTextAtSize(processedTexts.footerRight, fontSize), y: margin });
|
});
|
||||||
|
if (processedTexts.headerCenter)
|
||||||
|
page.drawText(processedTexts.headerCenter, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width / 2 -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.headerCenter,
|
||||||
|
fontSize
|
||||||
|
) /
|
||||||
|
2,
|
||||||
|
y: height - margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.headerRight)
|
||||||
|
page.drawText(processedTexts.headerRight, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width -
|
||||||
|
margin -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.headerRight,
|
||||||
|
fontSize
|
||||||
|
),
|
||||||
|
y: height - margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.footerLeft)
|
||||||
|
page.drawText(processedTexts.footerLeft, {
|
||||||
|
...drawOptions,
|
||||||
|
x: margin,
|
||||||
|
y: margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.footerCenter)
|
||||||
|
page.drawText(processedTexts.footerCenter, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width / 2 -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.footerCenter,
|
||||||
|
fontSize
|
||||||
|
) /
|
||||||
|
2,
|
||||||
|
y: margin,
|
||||||
|
});
|
||||||
|
if (processedTexts.footerRight)
|
||||||
|
page.drawText(processedTexts.footerRight, {
|
||||||
|
...drawOptions,
|
||||||
|
x:
|
||||||
|
width -
|
||||||
|
margin -
|
||||||
|
helveticaFont.widthOfTextAtSize(
|
||||||
|
processedTexts.footerRight,
|
||||||
|
fontSize
|
||||||
|
),
|
||||||
|
y: margin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'header-footer-added.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'header-footer-added.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Could not add header or footer.');
|
showAlert('Error', e.message || 'Could not add header or footer.');
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export async function addPageNumbers() {
|
|||||||
const xOffset = bounds.x || 0;
|
const xOffset = bounds.x || 0;
|
||||||
const yOffset = bounds.y || 0;
|
const yOffset = bounds.y || 0;
|
||||||
|
|
||||||
let pageNumText = (format === 'page_x_of_y') ? `${i + 1} / ${totalPages}` : `${i + 1}`;
|
let pageNumText =
|
||||||
|
format === 'page_x_of_y' ? `${i + 1} / ${totalPages}` : `${i + 1}`;
|
||||||
|
|
||||||
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
|
const textWidth = helveticaFont.widthOfTextAtSize(pageNumText, fontSize);
|
||||||
const textHeight = fontSize;
|
const textHeight = fontSize;
|
||||||
@@ -41,18 +42,34 @@ export async function addPageNumbers() {
|
|||||||
const maxMargin = 40;
|
const maxMargin = 40;
|
||||||
const marginPercentage = 0.04;
|
const marginPercentage = 0.04;
|
||||||
|
|
||||||
const horizontalMargin = Math.max(minMargin, Math.min(maxMargin, width * marginPercentage));
|
const horizontalMargin = Math.max(
|
||||||
const verticalMargin = Math.max(minMargin, Math.min(maxMargin, height * marginPercentage));
|
minMargin,
|
||||||
|
Math.min(maxMargin, width * marginPercentage)
|
||||||
|
);
|
||||||
|
const verticalMargin = Math.max(
|
||||||
|
minMargin,
|
||||||
|
Math.min(maxMargin, height * marginPercentage)
|
||||||
|
);
|
||||||
|
|
||||||
// Ensure text doesn't go outside visible page boundaries
|
// Ensure text doesn't go outside visible page boundaries
|
||||||
const safeHorizontalMargin = Math.max(horizontalMargin, textWidth / 2 + 3);
|
const safeHorizontalMargin = Math.max(
|
||||||
|
horizontalMargin,
|
||||||
|
textWidth / 2 + 3
|
||||||
|
);
|
||||||
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
const safeVerticalMargin = Math.max(verticalMargin, textHeight + 3);
|
||||||
|
|
||||||
let x, y;
|
let x, y;
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'bottom-center':
|
case 'bottom-center':
|
||||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
Math.min(
|
||||||
|
width - safeHorizontalMargin - textWidth,
|
||||||
|
(width - textWidth) / 2
|
||||||
|
)
|
||||||
|
) + xOffset;
|
||||||
y = safeVerticalMargin + yOffset;
|
y = safeVerticalMargin + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'bottom-left':
|
case 'bottom-left':
|
||||||
@@ -60,11 +77,22 @@ export async function addPageNumbers() {
|
|||||||
y = safeVerticalMargin + yOffset;
|
y = safeVerticalMargin + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'bottom-right':
|
case 'bottom-right':
|
||||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
width - safeHorizontalMargin - textWidth
|
||||||
|
) + xOffset;
|
||||||
y = safeVerticalMargin + yOffset;
|
y = safeVerticalMargin + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'top-center':
|
case 'top-center':
|
||||||
x = Math.max(safeHorizontalMargin, Math.min(width - safeHorizontalMargin - textWidth, (width - textWidth) / 2)) + xOffset;
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
Math.min(
|
||||||
|
width - safeHorizontalMargin - textWidth,
|
||||||
|
(width - textWidth) / 2
|
||||||
|
)
|
||||||
|
) + xOffset;
|
||||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'top-left':
|
case 'top-left':
|
||||||
@@ -72,7 +100,11 @@ export async function addPageNumbers() {
|
|||||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||||
break;
|
break;
|
||||||
case 'top-right':
|
case 'top-right':
|
||||||
x = Math.max(safeHorizontalMargin, width - safeHorizontalMargin - textWidth) + xOffset;
|
x =
|
||||||
|
Math.max(
|
||||||
|
safeHorizontalMargin,
|
||||||
|
width - safeHorizontalMargin - textWidth
|
||||||
|
) + xOffset;
|
||||||
y = height - safeVerticalMargin - textHeight + yOffset;
|
y = height - safeVerticalMargin - textHeight + yOffset;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -82,15 +114,19 @@ export async function addPageNumbers() {
|
|||||||
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
|
y = Math.max(yOffset + 3, Math.min(yOffset + height - textHeight - 3, y));
|
||||||
|
|
||||||
page.drawText(pageNumText, {
|
page.drawText(pageNumText, {
|
||||||
x, y,
|
x,
|
||||||
|
y,
|
||||||
font: helveticaFont,
|
font: helveticaFont,
|
||||||
size: fontSize,
|
size: fontSize,
|
||||||
color: rgb(textColor.r, textColor.g, textColor.b)
|
color: rgb(textColor.r, textColor.g, textColor.b),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'paginated.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'paginated.pdf'
|
||||||
|
);
|
||||||
showAlert('Success', 'Page numbers added successfully!');
|
showAlert('Success', 'Page numbers added successfully!');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
+100
-37
@@ -1,17 +1,27 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, hexToRgb } from '../utils/helpers.js';
|
import {
|
||||||
import { state } from '../state.js';
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
hexToRgb,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
|
import { state, resetState } from '../state.js';
|
||||||
|
|
||||||
import { PDFDocument as PDFLibDocument, rgb, degrees, StandardFonts } from 'pdf-lib';
|
import {
|
||||||
|
PDFDocument as PDFLibDocument,
|
||||||
|
rgb,
|
||||||
|
degrees,
|
||||||
|
StandardFonts,
|
||||||
|
} from 'pdf-lib';
|
||||||
|
|
||||||
export function setupWatermarkUI() {
|
export function setupWatermarkUI() {
|
||||||
const watermarkTypeRadios = document.querySelectorAll('input[name="watermark-type"]');
|
const watermarkTypeRadios = document.querySelectorAll(
|
||||||
|
'input[name="watermark-type"]'
|
||||||
|
);
|
||||||
const textOptions = document.getElementById('text-watermark-options');
|
const textOptions = document.getElementById('text-watermark-options');
|
||||||
const imageOptions = document.getElementById('image-watermark-options');
|
const imageOptions = document.getElementById('image-watermark-options');
|
||||||
|
|
||||||
watermarkTypeRadios.forEach(radio => {
|
watermarkTypeRadios.forEach((radio) => {
|
||||||
radio.addEventListener('change', (e) => {
|
radio.addEventListener('change', (e) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
|
||||||
if (e.target.value === 'text') {
|
if (e.target.value === 'text') {
|
||||||
textOptions.classList.remove('hidden');
|
textOptions.classList.remove('hidden');
|
||||||
imageOptions.classList.add('hidden');
|
imageOptions.classList.add('hidden');
|
||||||
@@ -27,25 +37,48 @@ export function setupWatermarkUI() {
|
|||||||
const angleSliderText = document.getElementById('angle-text');
|
const angleSliderText = document.getElementById('angle-text');
|
||||||
const angleValueText = document.getElementById('angle-value-text');
|
const angleValueText = document.getElementById('angle-value-text');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
opacitySliderText.addEventListener(
|
||||||
opacitySliderText.addEventListener('input', () => opacityValueText.textContent = opacitySliderText.value);
|
'input',
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
() =>
|
||||||
angleSliderText.addEventListener('input', () => angleValueText.textContent = angleSliderText.value);
|
(opacityValueText.textContent = (
|
||||||
|
opacitySliderText as HTMLInputElement
|
||||||
|
).value)
|
||||||
|
);
|
||||||
|
|
||||||
|
angleSliderText.addEventListener(
|
||||||
|
'input',
|
||||||
|
() =>
|
||||||
|
(angleValueText.textContent = (angleSliderText as HTMLInputElement).value)
|
||||||
|
);
|
||||||
|
|
||||||
const opacitySliderImage = document.getElementById('opacity-image');
|
const opacitySliderImage = document.getElementById('opacity-image');
|
||||||
const opacityValueImage = document.getElementById('opacity-value-image');
|
const opacityValueImage = document.getElementById('opacity-value-image');
|
||||||
const angleSliderImage = document.getElementById('angle-image');
|
const angleSliderImage = document.getElementById('angle-image');
|
||||||
const angleValueImage = document.getElementById('angle-value-image');
|
const angleValueImage = document.getElementById('angle-value-image');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
opacitySliderImage.addEventListener(
|
||||||
opacitySliderImage.addEventListener('input', () => opacityValueImage.textContent = opacitySliderImage.value);
|
'input',
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
() =>
|
||||||
angleSliderImage.addEventListener('input', () => angleValueImage.textContent = angleSliderImage.value);
|
(opacityValueImage.textContent = (
|
||||||
|
opacitySliderImage as HTMLInputElement
|
||||||
|
).value)
|
||||||
|
);
|
||||||
|
|
||||||
|
angleSliderImage.addEventListener(
|
||||||
|
'input',
|
||||||
|
() =>
|
||||||
|
(angleValueImage.textContent = (
|
||||||
|
angleSliderImage as HTMLInputElement
|
||||||
|
).value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addWatermark() {
|
export async function addWatermark() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
const watermarkType = (
|
||||||
const watermarkType = document.querySelector('input[name="watermark-type"]:checked').value;
|
document.querySelector(
|
||||||
|
'input[name="watermark-type"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
|
||||||
showLoader('Adding watermark...');
|
showLoader('Adding watermark...');
|
||||||
|
|
||||||
@@ -55,10 +88,13 @@ export async function addWatermark() {
|
|||||||
|
|
||||||
if (watermarkType === 'text') {
|
if (watermarkType === 'text') {
|
||||||
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
watermarkAsset = await state.pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
} else { // 'image'
|
} else {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// 'image'
|
||||||
const imageFile = document.getElementById('image-watermark-input').files[0];
|
const imageFile = (
|
||||||
if (!imageFile) throw new Error('Please select an image file for the watermark.');
|
document.getElementById('image-watermark-input') as HTMLInputElement
|
||||||
|
).files?.[0];
|
||||||
|
if (!imageFile)
|
||||||
|
throw new Error('Please select an image file for the watermark.');
|
||||||
|
|
||||||
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
const imageBytes = await readFileAsArrayBuffer(imageFile);
|
||||||
if (imageFile.type === 'image/png') {
|
if (imageFile.type === 'image/png') {
|
||||||
@@ -66,7 +102,9 @@ export async function addWatermark() {
|
|||||||
} else if (imageFile.type === 'image/jpeg') {
|
} else if (imageFile.type === 'image/jpeg') {
|
||||||
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
|
watermarkAsset = await state.pdfDoc.embedJpg(imageBytes);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unsupported Image. Please use a PNG or JPG for the watermark.');
|
throw new Error(
|
||||||
|
'Unsupported Image. Please use a PNG or JPG for the watermark.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,16 +114,24 @@ export async function addWatermark() {
|
|||||||
if (watermarkType === 'text') {
|
if (watermarkType === 'text') {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const text = document.getElementById('watermark-text').value;
|
const text = document.getElementById('watermark-text').value;
|
||||||
if (!text.trim()) throw new Error('Please enter text for the watermark.');
|
if (!text.trim())
|
||||||
|
throw new Error('Please enter text for the watermark.');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const fontSize =
|
||||||
const fontSize = parseInt(document.getElementById('font-size').value) || 72;
|
parseInt(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
(document.getElementById('font-size') as HTMLInputElement).value
|
||||||
const angle = parseInt(document.getElementById('angle-text').value) || 0;
|
) || 72;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const angle =
|
||||||
const opacity = parseFloat(document.getElementById('opacity-text').value) || 0.3;
|
parseInt(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
(document.getElementById('angle-text') as HTMLInputElement).value
|
||||||
const colorHex = document.getElementById('text-color').value;
|
) || 0;
|
||||||
|
const opacity =
|
||||||
|
parseFloat(
|
||||||
|
(document.getElementById('opacity-text') as HTMLInputElement).value
|
||||||
|
) || 0.3;
|
||||||
|
const colorHex = (
|
||||||
|
document.getElementById('text-color') as HTMLInputElement
|
||||||
|
).value;
|
||||||
const textColor = hexToRgb(colorHex);
|
const textColor = hexToRgb(colorHex);
|
||||||
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
const textWidth = watermarkAsset.widthOfTextAtSize(text, fontSize);
|
||||||
|
|
||||||
@@ -98,12 +144,15 @@ export async function addWatermark() {
|
|||||||
opacity: opacity,
|
opacity: opacity,
|
||||||
rotate: degrees(angle),
|
rotate: degrees(angle),
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const angle =
|
||||||
const angle = parseInt(document.getElementById('angle-image').value) || 0;
|
parseInt(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
(document.getElementById('angle-image') as HTMLInputElement).value
|
||||||
const opacity = parseFloat(document.getElementById('opacity-image').value) || 0.3;
|
) || 0;
|
||||||
|
const opacity =
|
||||||
|
parseFloat(
|
||||||
|
(document.getElementById('opacity-image') as HTMLInputElement).value
|
||||||
|
) || 0.3;
|
||||||
|
|
||||||
const scale = 0.5;
|
const scale = 0.5;
|
||||||
const imgWidth = watermarkAsset.width * scale;
|
const imgWidth = watermarkAsset.width * scale;
|
||||||
@@ -121,11 +170,25 @@ export async function addWatermark() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'watermarked.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'watermarked.pdf'
|
||||||
|
);
|
||||||
|
|
||||||
|
const toolid = state.activeTool;
|
||||||
|
resetState();
|
||||||
|
if (toolid) {
|
||||||
|
const element = document.querySelector(
|
||||||
|
`[data-tool-id="${toolid}"]`
|
||||||
|
) as HTMLElement;
|
||||||
|
if (element) element.click();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Could not add the watermark. Please check your inputs.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
e.message || 'Could not add the watermark. Please check your inputs.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
import Sortable from 'sortablejs';
|
||||||
|
|
||||||
|
const alternateMergeState = {
|
||||||
|
pdfDocs: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function setupAlternateMergeTool() {
|
||||||
|
const optionsDiv = document.getElementById('alternate-merge-options');
|
||||||
|
const processBtn = document.getElementById(
|
||||||
|
'process-btn'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const fileList = document.getElementById('alternate-file-list');
|
||||||
|
|
||||||
|
if (!optionsDiv || !processBtn || !fileList) return;
|
||||||
|
|
||||||
|
optionsDiv.classList.remove('hidden');
|
||||||
|
processBtn.disabled = false;
|
||||||
|
processBtn.onclick = alternateMerge;
|
||||||
|
|
||||||
|
fileList.innerHTML = '';
|
||||||
|
alternateMergeState.pdfDocs = {};
|
||||||
|
|
||||||
|
showLoader('Loading PDF documents...');
|
||||||
|
try {
|
||||||
|
for (const file of state.files) {
|
||||||
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
|
alternateMergeState.pdfDocs[file.name] = await PDFDocument.load(
|
||||||
|
pdfBytes as ArrayBuffer,
|
||||||
|
{
|
||||||
|
ignoreEncryption: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const pageCount = alternateMergeState.pdfDocs[file.name].getPageCount();
|
||||||
|
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className =
|
||||||
|
'bg-gray-700 p-3 rounded-lg border border-gray-600 flex items-center justify-between';
|
||||||
|
li.dataset.fileName = file.name;
|
||||||
|
|
||||||
|
const infoDiv = document.createElement('div');
|
||||||
|
infoDiv.className = 'flex items-center gap-2 truncate';
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('span');
|
||||||
|
nameSpan.className = 'truncate font-medium text-white';
|
||||||
|
nameSpan.textContent = file.name;
|
||||||
|
|
||||||
|
const pagesSpan = document.createElement('span');
|
||||||
|
pagesSpan.className = 'text-sm text-gray-400 flex-shrink-0';
|
||||||
|
pagesSpan.textContent = `(${pageCount} pages)`;
|
||||||
|
|
||||||
|
infoDiv.append(nameSpan, pagesSpan);
|
||||||
|
|
||||||
|
const dragHandle = document.createElement('div');
|
||||||
|
dragHandle.className =
|
||||||
|
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded';
|
||||||
|
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`;
|
||||||
|
|
||||||
|
li.append(infoDiv, dragHandle);
|
||||||
|
fileList.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
Sortable.create(fileList, {
|
||||||
|
handle: '.drag-handle',
|
||||||
|
animation: 150,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to load one or more PDF files. They may be corrupted or password-protected.'
|
||||||
|
);
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function alternateMerge() {
|
||||||
|
if (Object.keys(alternateMergeState.pdfDocs).length < 2) {
|
||||||
|
showAlert(
|
||||||
|
'Not Enough Files',
|
||||||
|
'Please upload at least two PDF files to alternate and mix.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Alternating and mixing pages...');
|
||||||
|
try {
|
||||||
|
const newPdfDoc = await PDFDocument.create();
|
||||||
|
const fileList = document.getElementById('alternate-file-list');
|
||||||
|
const sortedFileNames = Array.from(fileList.children).map(
|
||||||
|
(li) => (li as HTMLElement).dataset.fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadedDocs = sortedFileNames.map(
|
||||||
|
(name) => alternateMergeState.pdfDocs[name]
|
||||||
|
);
|
||||||
|
const pageCounts = loadedDocs.map((doc) => doc.getPageCount());
|
||||||
|
const maxPages = Math.max(...pageCounts);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxPages; i++) {
|
||||||
|
for (const doc of loadedDocs) {
|
||||||
|
if (i < doc.getPageCount()) {
|
||||||
|
const [copiedPage] = await newPdfDoc.copyPages(doc, [i]);
|
||||||
|
newPdfDoc.addPage(copiedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedPdfBytes = await newPdfDoc.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'alternated-mixed.pdf'
|
||||||
|
);
|
||||||
|
showAlert('Success', 'PDFs have been mixed successfully!');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Alternate Merge error:', e);
|
||||||
|
showAlert('Error', 'An error occurred while mixing the PDFs.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,9 @@ async function convertImageToPngBytes(file: any) {
|
|||||||
canvas.height = img.height;
|
canvas.height = img.height;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
const pngBlob = await new Promise(res => canvas.toBlob(res, 'image/png'));
|
const pngBlob = await new Promise((res) =>
|
||||||
|
canvas.toBlob(res, 'image/png')
|
||||||
|
);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
const pngBytes = await pngBlob.arrayBuffer();
|
||||||
resolve(pngBytes);
|
resolve(pngBytes);
|
||||||
@@ -42,13 +44,24 @@ export async function bmpToPdf() {
|
|||||||
const pngBytes = await convertImageToPngBytes(file);
|
const pngBytes = await convertImageToPngBytes(file);
|
||||||
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
const pngImage = await pdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
page.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: pngImage.width,
|
||||||
|
height: pngImage.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_bmps.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_bmps.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert BMP to PDF. One of the files may be invalid.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert BMP to PDF. One of the files may be invalid.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -43,7 +42,10 @@ export async function changeBackgroundColor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'background-changed.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'background-changed.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not change the background color.');
|
showAlert('Error', 'Could not change the background color.');
|
||||||
|
|||||||
@@ -1,115 +1,169 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
initializeQpdf,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
|
||||||
import blobStream from 'blob-stream';
|
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
|
||||||
|
|
||||||
export async function changePermissions() {
|
export async function changePermissions() {
|
||||||
const currentPassword = (document.getElementById('current-password') as HTMLInputElement).value;
|
|
||||||
const newUserPassword = (document.getElementById('new-user-password') as HTMLInputElement).value;
|
|
||||||
const newOwnerPassword = (document.getElementById('new-owner-password') as HTMLInputElement).value;
|
|
||||||
|
|
||||||
// An owner password is required to enforce any permissions.
|
|
||||||
if (!newOwnerPassword && (newUserPassword || document.querySelectorAll('input[type="checkbox"]:not(:checked)').length > 0)) {
|
|
||||||
showAlert('Input Required', 'You must set a "New Owner Password" to enforce specific permissions or to set a user password.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
showLoader('Preparing to process...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
const pdfData = await readFileAsArrayBuffer(file);
|
const currentPassword =
|
||||||
|
(document.getElementById('current-password') as HTMLInputElement)?.value ||
|
||||||
|
'';
|
||||||
|
const newUserPassword =
|
||||||
|
(document.getElementById('new-user-password') as HTMLInputElement)?.value ||
|
||||||
|
'';
|
||||||
|
const newOwnerPassword =
|
||||||
|
(document.getElementById('new-owner-password') as HTMLInputElement)
|
||||||
|
?.value || '';
|
||||||
|
|
||||||
|
const inputPath = '/input.pdf';
|
||||||
|
const outputPath = '/output.pdf';
|
||||||
|
let qpdf: any;
|
||||||
|
|
||||||
let pdf;
|
|
||||||
try {
|
try {
|
||||||
pdf = await pdfjsLib.getDocument({
|
showLoader('Initializing...');
|
||||||
data: pdfData as ArrayBuffer,
|
qpdf = await initializeQpdf();
|
||||||
password: currentPassword
|
|
||||||
}).promise;
|
showLoader('Reading PDF...');
|
||||||
} catch (e) {
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
// This catch is specific to password errors in pdf.js
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
if (e.name === 'PasswordException') {
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
hideLoader();
|
|
||||||
showAlert('Incorrect Password', 'The current password you entered is incorrect.');
|
showLoader('Processing PDF permissions...');
|
||||||
return;
|
|
||||||
}
|
const args = [inputPath];
|
||||||
throw e;
|
|
||||||
|
// Add password if provided
|
||||||
|
if (currentPassword) {
|
||||||
|
args.push('--password=' + currentPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
const numPages = pdf.numPages;
|
const shouldEncrypt = newUserPassword || newOwnerPassword;
|
||||||
const pageImages = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= numPages; i++) {
|
if (shouldEncrypt) {
|
||||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
const finalUserPassword = newUserPassword;
|
||||||
const page = await pdf.getPage(i);
|
const finalOwnerPassword = newOwnerPassword;
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
|
||||||
const canvas = document.createElement('canvas');
|
args.push('--encrypt', finalUserPassword, finalOwnerPassword, '256');
|
||||||
canvas.height = viewport.height;
|
|
||||||
canvas.width = viewport.width;
|
const allowPrinting = (
|
||||||
const context = canvas.getContext('2d');
|
document.getElementById('allow-printing') as HTMLInputElement
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
)?.checked;
|
||||||
pageImages.push({
|
const allowCopying = (
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
document.getElementById('allow-copying') as HTMLInputElement
|
||||||
width: viewport.width,
|
)?.checked;
|
||||||
height: viewport.height
|
const allowModifying = (
|
||||||
});
|
document.getElementById('allow-modifying') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
const allowAnnotating = (
|
||||||
|
document.getElementById('allow-annotating') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
const allowFillingForms = (
|
||||||
|
document.getElementById('allow-filling-forms') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
const allowDocumentAssembly = (
|
||||||
|
document.getElementById('allow-document-assembly') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
const allowPageExtraction = (
|
||||||
|
document.getElementById('allow-page-extraction') as HTMLInputElement
|
||||||
|
)?.checked;
|
||||||
|
|
||||||
|
if (finalOwnerPassword) {
|
||||||
|
if (!allowModifying) args.push('--modify=none');
|
||||||
|
if (!allowCopying) args.push('--extract=n');
|
||||||
|
if (!allowPrinting) args.push('--print=none');
|
||||||
|
if (!allowAnnotating) args.push('--annotate=n');
|
||||||
|
if (!allowDocumentAssembly) args.push('--assemble=n');
|
||||||
|
if (!allowFillingForms) args.push('--form=n');
|
||||||
|
if (!allowPageExtraction) args.push('--extract=n');
|
||||||
|
// --modify-other is not directly mapped, apply if modifying is disabled
|
||||||
|
if (!allowModifying) args.push('--modify-other=n');
|
||||||
|
} else if (finalUserPassword) {
|
||||||
|
args.push('--allow-insecure');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args.push('--decrypt');
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent = 'Applying new permissions...';
|
args.push('--', outputPath);
|
||||||
|
try {
|
||||||
|
qpdf.callMain(args);
|
||||||
|
} catch (qpdfError: any) {
|
||||||
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
|
||||||
const allowPrinting = (document.getElementById('allow-printing') as HTMLInputElement).checked;
|
const errorMsg = qpdfError.message || '';
|
||||||
const allowCopying = (document.getElementById('allow-copying') as HTMLInputElement).checked;
|
|
||||||
const allowModifying = (document.getElementById('allow-modifying') as HTMLInputElement).checked;
|
|
||||||
const allowAnnotating = (document.getElementById('allow-annotating') as HTMLInputElement).checked;
|
|
||||||
const allowFillingForms = (document.getElementById('allow-filling-forms') as HTMLInputElement).checked;
|
|
||||||
const allowContentAccessibility = (document.getElementById('allow-content-accessibility') as HTMLInputElement).checked;
|
|
||||||
const allowDocumentAssembly = (document.getElementById('allow-document-assembly') as HTMLInputElement).checked;
|
|
||||||
|
|
||||||
|
if (
|
||||||
const doc = new PDFDocument({
|
errorMsg.includes('invalid password') ||
|
||||||
size: [pageImages[0].width, pageImages[0].height],
|
errorMsg.includes('incorrect password') ||
|
||||||
pdfVersion: '1.7ext3', // Uses 256-bit AES encryption
|
errorMsg.includes('password')
|
||||||
|
) {
|
||||||
// Apply the new, separate user and owner passwords
|
throw new Error('INVALID_PASSWORD');
|
||||||
userPassword: newUserPassword,
|
|
||||||
ownerPassword: newOwnerPassword,
|
|
||||||
|
|
||||||
// Apply all seven permissions from the checkboxes
|
|
||||||
permissions: {
|
|
||||||
printing: allowPrinting ? 'highResolution' : false,
|
|
||||||
modifying: allowModifying,
|
|
||||||
copying: allowCopying,
|
|
||||||
annotating: allowAnnotating,
|
|
||||||
fillingForms: allowFillingForms,
|
|
||||||
contentAccessibility: allowContentAccessibility,
|
|
||||||
documentAssembly: allowDocumentAssembly
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const stream = doc.pipe(blobStream());
|
|
||||||
|
|
||||||
for (let i = 0; i < pageImages.length; i++) {
|
|
||||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
|
||||||
doc.image(pageImages[i].data, 0, 0, {
|
|
||||||
width: pageImages[i].width,
|
|
||||||
height: pageImages[i].height
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.end();
|
if (
|
||||||
stream.on('finish', function () {
|
errorMsg.includes('encrypted') ||
|
||||||
const blob = stream.toBlob('application/pdf');
|
errorMsg.includes('password required')
|
||||||
|
) {
|
||||||
|
throw new Error('PASSWORD_REQUIRED');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Processing failed: ' + errorMsg || 'Unknown error');
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Preparing download...');
|
||||||
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
|
||||||
|
if (!outputFile || outputFile.length === 0) {
|
||||||
|
throw new Error('Processing resulted in an empty file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
downloadFile(blob, `permissions-changed-${file.name}`);
|
downloadFile(blob, `permissions-changed-${file.name}`);
|
||||||
hideLoader();
|
|
||||||
showAlert('Success', 'Permissions changed successfully!');
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', `An unexpected error occurred: ${e.message}`);
|
|
||||||
|
let successMessage = 'PDF permissions changed successfully!';
|
||||||
|
if (!shouldEncrypt) {
|
||||||
|
successMessage =
|
||||||
|
'PDF decrypted successfully! All encryption and restrictions removed.';
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert('Success', successMessage);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error during PDF permission change:', error);
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
if (error.message === 'INVALID_PASSWORD') {
|
||||||
|
showAlert(
|
||||||
|
'Incorrect Password',
|
||||||
|
'The current password you entered is incorrect. Please try again.'
|
||||||
|
);
|
||||||
|
} else if (error.message === 'PASSWORD_REQUIRED') {
|
||||||
|
showAlert(
|
||||||
|
'Password Required',
|
||||||
|
'This PDF is password-protected. Please enter the current password to proceed.'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showAlert(
|
||||||
|
'Processing Failed',
|
||||||
|
`An error occurred: ${error.message || 'The PDF might be corrupted or password protected.'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (qpdf?.FS) {
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(inputPath);
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(outputPath);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
hexToRgb,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
@@ -17,7 +20,9 @@ async function updateTextColorPreview() {
|
|||||||
if (!textColorCanvas) return;
|
if (!textColorCanvas) return;
|
||||||
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const page = await pdf.getPage(1); // Preview first page
|
const page = await pdf.getPage(1); // Preview first page
|
||||||
const viewport = page.getViewport({ scale: 0.8 });
|
const viewport = page.getViewport({ scale: 0.8 });
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
||||||
@@ -29,17 +34,25 @@ async function updateTextColorPreview() {
|
|||||||
textColorCanvas.height = viewport.height;
|
textColorCanvas.height = viewport.height;
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
|
const imageData = context.getImageData(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'width' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
0,
|
||||||
const imageData = context.getImageData(0, 0, textColorCanvas.width, textColorCanvas.height);
|
0,
|
||||||
|
(textColorCanvas as HTMLCanvasElement).width,
|
||||||
|
(textColorCanvas as HTMLCanvasElement).height
|
||||||
|
);
|
||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const colorHex = (
|
||||||
const colorHex = document.getElementById('text-color-input').value;
|
document.getElementById('text-color-input') as HTMLInputElement
|
||||||
|
).value;
|
||||||
const { r, g, b } = hexToRgb(colorHex);
|
const { r, g, b } = hexToRgb(colorHex);
|
||||||
const darknessThreshold = 120;
|
const darknessThreshold = 120;
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
if (data[i] < darknessThreshold && data[i + 1] < darknessThreshold && data[i + 2] < darknessThreshold) {
|
if (
|
||||||
|
data[i] < darknessThreshold &&
|
||||||
|
data[i + 1] < darknessThreshold &&
|
||||||
|
data[i + 2] < darknessThreshold
|
||||||
|
) {
|
||||||
data[i] = r * 255;
|
data[i] = r * 255;
|
||||||
data[i + 1] = g * 255;
|
data[i + 1] = g * 255;
|
||||||
data[i + 2] = b * 255;
|
data[i + 2] = b * 255;
|
||||||
@@ -66,7 +79,9 @@ export async function setupTextColorTool() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const page = await pdf.getPage(1);
|
const page = await pdf.getPage(1);
|
||||||
const viewport = page.getViewport({ scale: 0.8 });
|
const viewport = page.getViewport({ scale: 0.8 });
|
||||||
|
|
||||||
@@ -75,8 +90,10 @@ export async function setupTextColorTool() {
|
|||||||
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'height' does not exist on type 'HTMLElem... Remove this comment to see the full error message
|
||||||
originalCanvas.height = viewport.height;
|
originalCanvas.height = viewport.height;
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'getContext' does not exist on type 'HTML... Remove this comment to see the full error message
|
await page.render({
|
||||||
await page.render({ canvasContext: originalCanvas.getContext('2d'), viewport }).promise;
|
canvasContext: (originalCanvas as HTMLCanvasElement).getContext('2d'),
|
||||||
|
viewport,
|
||||||
|
}).promise;
|
||||||
await updateTextColorPreview();
|
await updateTextColorPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +112,9 @@ export async function changeTextColor() {
|
|||||||
try {
|
try {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
showLoader(`Processing page ${i} of ${pdf.numPages}...`);
|
||||||
@@ -113,7 +132,11 @@ export async function changeTextColor() {
|
|||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
|
|
||||||
for (let j = 0; j < data.length; j += 4) {
|
for (let j = 0; j < data.length; j += 4) {
|
||||||
if (data[j] < darknessThreshold && data[j + 1] < darknessThreshold && data[j + 2] < darknessThreshold) {
|
if (
|
||||||
|
data[j] < darknessThreshold &&
|
||||||
|
data[j + 1] < darknessThreshold &&
|
||||||
|
data[j + 2] < darknessThreshold
|
||||||
|
) {
|
||||||
data[j] = r * 255;
|
data[j] = r * 255;
|
||||||
data[j + 1] = g * 255;
|
data[j + 1] = g * 255;
|
||||||
data[j + 2] = b * 255;
|
data[j + 2] = b * 255;
|
||||||
@@ -121,20 +144,30 @@ export async function changeTextColor() {
|
|||||||
}
|
}
|
||||||
context.putImageData(imageData, 0, 0);
|
context.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
const pngImageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}, 'image/png'));
|
}, 'image/png')
|
||||||
|
);
|
||||||
|
|
||||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
newPage.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'text-color-changed.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'text-color-changed.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not change text color.');
|
showAlert('Error', 'Could not change text color.');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -53,7 +52,7 @@ export async function combineToSinglePage() {
|
|||||||
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
newPage.drawPage(embeddedPage, { x, y: currentY, width, height });
|
||||||
|
|
||||||
if (addSeparator && i < sourcePages.length - 1) {
|
if (addSeparator && i < sourcePages.length - 1) {
|
||||||
const lineY = currentY - (spacing / 2);
|
const lineY = currentY - spacing / 2;
|
||||||
newPage.drawLine({
|
newPage.drawLine({
|
||||||
start: { x: 0, y: lineY },
|
start: { x: 0, y: lineY },
|
||||||
end: { x: maxWidth, y: lineY },
|
end: { x: maxWidth, y: lineY },
|
||||||
@@ -66,8 +65,10 @@ export async function combineToSinglePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
const newPdfBytes = await newDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'combined-page.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'combined-page.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while combining pages.');
|
showAlert('Error', 'An error occurred while combining pages.');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { icons, createIcons } from "lucide";
|
import { icons, createIcons } from 'lucide';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
pdfDoc1: null,
|
pdfDoc1: null,
|
||||||
@@ -17,7 +17,12 @@ const state = {
|
|||||||
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
|
* @param {HTMLCanvasElement} canvas - The canvas element to draw on.
|
||||||
* @param {HTMLElement} container - The container to fit the canvas into.
|
* @param {HTMLElement} container - The container to fit the canvas into.
|
||||||
*/
|
*/
|
||||||
async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any) {
|
async function renderPage(
|
||||||
|
pdfDoc: any,
|
||||||
|
pageNum: any,
|
||||||
|
canvas: any,
|
||||||
|
container: any
|
||||||
|
) {
|
||||||
const page = await pdfDoc.getPage(pageNum);
|
const page = await pdfDoc.getPage(pageNum);
|
||||||
|
|
||||||
// Calculate scale to fit the container width.
|
// Calculate scale to fit the container width.
|
||||||
@@ -35,7 +40,6 @@ async function renderPage(pdfDoc: any, pageNum: any, canvas: any, container: any
|
|||||||
}).promise;
|
}).promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function renderBothPages() {
|
async function renderBothPages() {
|
||||||
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
if (!state.pdfDoc1 || !state.pdfDoc2) return;
|
||||||
|
|
||||||
@@ -52,8 +56,18 @@ async function renderBothPages() {
|
|||||||
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
|
const container2 = state.viewMode === 'overlay' ? wrapper : panel2;
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
renderPage(state.pdfDoc1, Math.min(state.currentPage, state.pdfDoc1.numPages), canvas1, container1),
|
renderPage(
|
||||||
renderPage(state.pdfDoc2, Math.min(state.currentPage, state.pdfDoc2.numPages), canvas2, container2)
|
state.pdfDoc1,
|
||||||
|
Math.min(state.currentPage, state.pdfDoc1.numPages),
|
||||||
|
canvas1,
|
||||||
|
container1
|
||||||
|
),
|
||||||
|
renderPage(
|
||||||
|
state.pdfDoc2,
|
||||||
|
Math.min(state.currentPage, state.pdfDoc2.numPages),
|
||||||
|
canvas2,
|
||||||
|
container2
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
updateNavControls();
|
updateNavControls();
|
||||||
@@ -61,15 +75,21 @@ async function renderBothPages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateNavControls() {
|
function updateNavControls() {
|
||||||
const maxPages = Math.max(state.pdfDoc1?.numPages || 0, state.pdfDoc2?.numPages || 0);
|
const maxPages = Math.max(
|
||||||
|
state.pdfDoc1?.numPages || 0,
|
||||||
|
state.pdfDoc2?.numPages || 0
|
||||||
|
);
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
document.getElementById('current-page-display-compare').textContent = state.currentPage;
|
document.getElementById('current-page-display-compare').textContent =
|
||||||
|
state.currentPage;
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
document.getElementById('total-pages-display-compare').textContent = maxPages;
|
document.getElementById('total-pages-display-compare').textContent = maxPages;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('prev-page-compare').disabled = state.currentPage <= 1;
|
document.getElementById('prev-page-compare').disabled =
|
||||||
|
state.currentPage <= 1;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('next-page-compare').disabled = state.currentPage >= maxPages;
|
document.getElementById('next-page-compare').disabled =
|
||||||
|
state.currentPage >= maxPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
||||||
@@ -77,7 +97,8 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
|||||||
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
|
const dropZone = document.getElementById(`drop-zone-${inputId.slice(-1)}`);
|
||||||
|
|
||||||
const handleFile = async (file: any) => {
|
const handleFile = async (file: any) => {
|
||||||
if (!file || file.type !== 'application/pdf') return showAlert('Invalid File', 'Please select a valid PDF file.');
|
if (!file || file.type !== 'application/pdf')
|
||||||
|
return showAlert('Invalid File', 'Please select a valid PDF file.');
|
||||||
|
|
||||||
const displayDiv = document.getElementById(displayId);
|
const displayDiv = document.getElementById(displayId);
|
||||||
displayDiv.textContent = '';
|
displayDiv.textContent = '';
|
||||||
@@ -110,7 +131,10 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
|||||||
await renderBothPages();
|
await renderBothPages();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showAlert('Error', 'Could not load PDF. It may be corrupt or password-protected.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not load PDF. It may be corrupt or password-protected.'
|
||||||
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -120,7 +144,10 @@ async function setupFileInput(inputId: any, docKey: any, displayId: any) {
|
|||||||
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'files' does not exist on type 'EventTarg... Remove this comment to see the full error message
|
||||||
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
|
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
|
||||||
dropZone.addEventListener('dragover', (e) => e.preventDefault());
|
dropZone.addEventListener('dragover', (e) => e.preventDefault());
|
||||||
dropZone.addEventListener('drop', (e) => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); });
|
dropZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleFile(e.dataTransfer.files[0]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,7 +164,6 @@ function setViewMode(mode: any) {
|
|||||||
const canvas2 = document.getElementById('canvas-compare-2');
|
const canvas2 = document.getElementById('canvas-compare-2');
|
||||||
const opacitySlider = document.getElementById('opacity-slider');
|
const opacitySlider = document.getElementById('opacity-slider');
|
||||||
|
|
||||||
|
|
||||||
if (mode === 'overlay') {
|
if (mode === 'overlay') {
|
||||||
wrapper.className = 'compare-viewer-wrapper overlay-mode';
|
wrapper.className = 'compare-viewer-wrapper overlay-mode';
|
||||||
overlayControls.classList.remove('hidden');
|
overlayControls.classList.remove('hidden');
|
||||||
@@ -163,20 +189,30 @@ export function setupCompareTool() {
|
|||||||
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
|
setupFileInput('file-input-2', 'pdfDoc2', 'file-display-2');
|
||||||
|
|
||||||
document.getElementById('prev-page-compare').addEventListener('click', () => {
|
document.getElementById('prev-page-compare').addEventListener('click', () => {
|
||||||
if (state.currentPage > 1) { state.currentPage--; renderBothPages(); }
|
if (state.currentPage > 1) {
|
||||||
|
state.currentPage--;
|
||||||
|
renderBothPages();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
document.getElementById('next-page-compare').addEventListener('click', () => {
|
document.getElementById('next-page-compare').addEventListener('click', () => {
|
||||||
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
const maxPages = Math.max(state.pdfDoc1.numPages, state.pdfDoc2.numPages);
|
||||||
if (state.currentPage < maxPages) { state.currentPage++; renderBothPages(); }
|
if (state.currentPage < maxPages) {
|
||||||
|
state.currentPage++;
|
||||||
|
renderBothPages();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('view-mode-overlay').addEventListener('click', () => setViewMode('overlay'));
|
document
|
||||||
document.getElementById('view-mode-side').addEventListener('click', () => setViewMode('side-by-side'));
|
.getElementById('view-mode-overlay')
|
||||||
|
.addEventListener('click', () => setViewMode('overlay'));
|
||||||
|
document
|
||||||
|
.getElementById('view-mode-side')
|
||||||
|
.addEventListener('click', () => setViewMode('side-by-side'));
|
||||||
|
|
||||||
const canvas2 = document.getElementById('canvas-compare-2');
|
const canvas2 = document.getElementById('canvas-compare-2');
|
||||||
document.getElementById('flicker-btn').addEventListener('click', () => {
|
document.getElementById('flicker-btn').addEventListener('click', () => {
|
||||||
canvas2.style.transition = 'opacity 150ms ease-in-out';
|
canvas2.style.transition = 'opacity 150ms ease-in-out';
|
||||||
canvas2.style.opacity = (canvas2.style.opacity === '0') ? '1' : '0';
|
canvas2.style.opacity = canvas2.style.opacity === '0' ? '1' : '0';
|
||||||
});
|
});
|
||||||
document.getElementById('opacity-slider').addEventListener('input', (e) => {
|
document.getElementById('opacity-slider').addEventListener('input', (e) => {
|
||||||
canvas2.style.transition = '';
|
canvas2.style.transition = '';
|
||||||
@@ -187,22 +223,23 @@ export function setupCompareTool() {
|
|||||||
const panel1 = document.getElementById('panel-1');
|
const panel1 = document.getElementById('panel-1');
|
||||||
const panel2 = document.getElementById('panel-2');
|
const panel2 = document.getElementById('panel-2');
|
||||||
const syncToggle = document.getElementById('sync-scroll-toggle');
|
const syncToggle = document.getElementById('sync-scroll-toggle');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
(syncToggle as HTMLInputElement).addEventListener('change', () => {
|
||||||
syncToggle.addEventListener('change', () => { state.isSyncScroll = syncToggle.checked; });
|
state.isSyncScroll = (syncToggle as HTMLInputElement).checked;
|
||||||
|
});
|
||||||
|
|
||||||
let scrollingPanel: any = null;
|
let scrollingPanel: any = null;
|
||||||
panel1.addEventListener('scroll', () => {
|
panel1.addEventListener('scroll', () => {
|
||||||
if (state.isSyncScroll && scrollingPanel !== panel2) {
|
if (state.isSyncScroll && scrollingPanel !== panel2) {
|
||||||
scrollingPanel = panel1;
|
scrollingPanel = panel1;
|
||||||
panel2.scrollTop = panel1.scrollTop;
|
panel2.scrollTop = panel1.scrollTop;
|
||||||
setTimeout(() => scrollingPanel = null, 100);
|
setTimeout(() => (scrollingPanel = null), 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
panel2.addEventListener('scroll', () => {
|
panel2.addEventListener('scroll', () => {
|
||||||
if (state.isSyncScroll && scrollingPanel !== panel1) {
|
if (state.isSyncScroll && scrollingPanel !== panel1) {
|
||||||
scrollingPanel = panel2;
|
scrollingPanel = panel2;
|
||||||
panel1.scrollTop = panel2.scrollTop;
|
panel1.scrollTop = panel2.scrollTop;
|
||||||
setTimeout(() => scrollingPanel = null, 100);
|
setTimeout(() => (scrollingPanel = null), 100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+102
-37
@@ -1,9 +1,12 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer, formatBytes } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
formatBytes,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
|
|
||||||
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
|
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
|
||||||
|
|
||||||
function dataUrlToBytes(dataUrl: any) {
|
function dataUrlToBytes(dataUrl: any) {
|
||||||
@@ -18,7 +21,9 @@ function dataUrlToBytes(dataUrl: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function performSmartCompression(arrayBuffer: any, settings: any) {
|
async function performSmartCompression(arrayBuffer: any, settings: any) {
|
||||||
const pdfDoc = await PDFDocument.load(arrayBuffer, { ignoreEncryption: true });
|
const pdfDoc = await PDFDocument.load(arrayBuffer, {
|
||||||
|
ignoreEncryption: true,
|
||||||
|
});
|
||||||
const pages = pdfDoc.getPages();
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
if (settings.removeMetadata) {
|
if (settings.removeMetadata) {
|
||||||
@@ -44,20 +49,29 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
|
|
||||||
for (const [key, value] of xobjects.entries()) {
|
for (const [key, value] of xobjects.entries()) {
|
||||||
const stream = pdfDoc.context.lookup(value);
|
const stream = pdfDoc.context.lookup(value);
|
||||||
if (!(stream instanceof PDFStream) || stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')) continue;
|
if (
|
||||||
|
!(stream instanceof PDFStream) ||
|
||||||
|
stream.dict.get(PDFName.of('Subtype')) !== PDFName.of('Image')
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const imageBytes = stream.getContents();
|
const imageBytes = stream.getContents();
|
||||||
if (imageBytes.length < settings.skipSize) continue;
|
if (imageBytes.length < settings.skipSize) continue;
|
||||||
|
|
||||||
const width = stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
|
const width =
|
||||||
|
stream.dict.get(PDFName.of('Width')) instanceof PDFNumber
|
||||||
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
|
? (stream.dict.get(PDFName.of('Width')) as PDFNumber).asNumber()
|
||||||
: 0;
|
: 0;
|
||||||
const height = stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
|
const height =
|
||||||
|
stream.dict.get(PDFName.of('Height')) instanceof PDFNumber
|
||||||
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
|
? (stream.dict.get(PDFName.of('Height')) as PDFNumber).asNumber()
|
||||||
: 0;
|
: 0;
|
||||||
const bitsPerComponent = stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
const bitsPerComponent =
|
||||||
? (stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber).asNumber()
|
stream.dict.get(PDFName.of('BitsPerComponent')) instanceof PDFNumber
|
||||||
|
? (
|
||||||
|
stream.dict.get(PDFName.of('BitsPerComponent')) as PDFNumber
|
||||||
|
).asNumber()
|
||||||
: 8;
|
: 8;
|
||||||
|
|
||||||
if (width > 0 && height > 0) {
|
if (width > 0 && height > 0) {
|
||||||
@@ -88,7 +102,9 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
canvas.height = Math.floor(newHeight);
|
canvas.height = Math.floor(newHeight);
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
const imageUrl = URL.createObjectURL(new Blob([new Uint8Array(imageBytes)]));
|
const imageUrl = URL.createObjectURL(
|
||||||
|
new Blob([new Uint8Array(imageBytes)])
|
||||||
|
);
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
img.onload = resolve;
|
img.onload = resolve;
|
||||||
@@ -119,13 +135,18 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
|
|
||||||
if (settings.tryWebP) {
|
if (settings.tryWebP) {
|
||||||
try {
|
try {
|
||||||
const webpDataUrl = canvas.toDataURL('image/webp', settings.quality);
|
const webpDataUrl = canvas.toDataURL(
|
||||||
|
'image/webp',
|
||||||
|
settings.quality
|
||||||
|
);
|
||||||
const webpBytes = dataUrlToBytes(webpDataUrl);
|
const webpBytes = dataUrlToBytes(webpDataUrl);
|
||||||
if (webpBytes.length < bestSize) {
|
if (webpBytes.length < bestSize) {
|
||||||
bestBytes = webpBytes;
|
bestBytes = webpBytes;
|
||||||
bestSize = webpBytes.length;
|
bestSize = webpBytes.length;
|
||||||
}
|
}
|
||||||
} catch (e) { /* WebP not supported */ }
|
} catch (e) {
|
||||||
|
/* WebP not supported */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
|
if (bestBytes && bestSize < imageBytes.length * settings.threshold) {
|
||||||
@@ -138,7 +159,10 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
|
stream.dict.set(PDFName.of('BitsPerComponent'), PDFNumber.of(8));
|
||||||
|
|
||||||
if (settings.grayscale) {
|
if (settings.grayscale) {
|
||||||
stream.dict.set(PDFName.of('ColorSpace'), PDFName.of('DeviceGray'));
|
stream.dict.set(
|
||||||
|
PDFName.of('ColorSpace'),
|
||||||
|
PDFName.of('DeviceGray')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
@@ -152,7 +176,7 @@ async function performSmartCompression(arrayBuffer: any, settings: any) {
|
|||||||
const saveOptions = {
|
const saveOptions = {
|
||||||
useObjectStreams: settings.useObjectStreams !== false,
|
useObjectStreams: settings.useObjectStreams !== false,
|
||||||
addDefaultPage: false,
|
addDefaultPage: false,
|
||||||
objectsPerTick: settings.objectsPerTick || 50
|
objectsPerTick: settings.objectsPerTick || 50,
|
||||||
};
|
};
|
||||||
|
|
||||||
return await pdfDoc.save(saveOptions);
|
return await pdfDoc.save(saveOptions);
|
||||||
@@ -175,14 +199,22 @@ async function performLegacyCompression(arrayBuffer: any, settings: any) {
|
|||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport, canvas: canvas }).promise;
|
await page.render({ canvasContext: context, viewport, canvas: canvas })
|
||||||
|
.promise;
|
||||||
|
|
||||||
const jpegBlob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', settings.quality));
|
const jpegBlob = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob(resolve, 'image/jpeg', settings.quality)
|
||||||
|
);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'arrayBuffer' does not exist on type 'unk... Remove this comment to see the full error message
|
||||||
const jpegBytes = await jpegBlob.arrayBuffer();
|
const jpegBytes = await jpegBlob.arrayBuffer();
|
||||||
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
const jpegImage = await newPdfDoc.embedJpg(jpegBytes);
|
||||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
newPage.drawImage(jpegImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
newPage.drawImage(jpegImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return await newPdfDoc.save();
|
return await newPdfDoc.save();
|
||||||
}
|
}
|
||||||
@@ -194,22 +226,46 @@ export async function compress() {
|
|||||||
const algorithm = document.getElementById('compression-algorithm').value;
|
const algorithm = document.getElementById('compression-algorithm').value;
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
'balanced': {
|
balanced: {
|
||||||
smart: { quality: 0.5, threshold: 0.95, maxWidth: 1800, maxHeight: 1800, skipSize: 3000 },
|
smart: {
|
||||||
legacy: { scale: 1.5, quality: 0.6 }
|
quality: 0.5,
|
||||||
|
threshold: 0.95,
|
||||||
|
maxWidth: 1800,
|
||||||
|
maxHeight: 1800,
|
||||||
|
skipSize: 3000,
|
||||||
|
},
|
||||||
|
legacy: { scale: 1.5, quality: 0.6 },
|
||||||
},
|
},
|
||||||
'high-quality': {
|
'high-quality': {
|
||||||
smart: { quality: 0.70, threshold: 0.98, maxWidth: 2500, maxHeight: 2500, skipSize: 5000 },
|
smart: {
|
||||||
legacy: { scale: 2.0, quality: 0.9 }
|
quality: 0.7,
|
||||||
|
threshold: 0.98,
|
||||||
|
maxWidth: 2500,
|
||||||
|
maxHeight: 2500,
|
||||||
|
skipSize: 5000,
|
||||||
|
},
|
||||||
|
legacy: { scale: 2.0, quality: 0.9 },
|
||||||
},
|
},
|
||||||
'small-size': {
|
'small-size': {
|
||||||
smart: { quality: 0.3, threshold: 0.95, maxWidth: 1200, maxHeight: 1200, skipSize: 2000 },
|
smart: {
|
||||||
legacy: { scale: 1.2, quality: 0.4 }
|
quality: 0.3,
|
||||||
|
threshold: 0.95,
|
||||||
|
maxWidth: 1200,
|
||||||
|
maxHeight: 1200,
|
||||||
|
skipSize: 2000,
|
||||||
|
},
|
||||||
|
legacy: { scale: 1.2, quality: 0.4 },
|
||||||
|
},
|
||||||
|
extreme: {
|
||||||
|
smart: {
|
||||||
|
quality: 0.1,
|
||||||
|
threshold: 0.95,
|
||||||
|
maxWidth: 1000,
|
||||||
|
maxHeight: 1000,
|
||||||
|
skipSize: 1000,
|
||||||
|
},
|
||||||
|
legacy: { scale: 1.0, quality: 0.2 },
|
||||||
},
|
},
|
||||||
'extreme': {
|
|
||||||
smart: { quality: 0.1, threshold: 0.95, maxWidth: 1000, maxHeight: 1000, skipSize: 1000 },
|
|
||||||
legacy: { scale: 1.0, quality: 0.2 }
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
const smartSettings = { ...settings[level].smart, removeMetadata: true };
|
||||||
@@ -232,16 +288,21 @@ export async function compress() {
|
|||||||
usedMethod = 'Photon';
|
usedMethod = 'Photon';
|
||||||
} else {
|
} else {
|
||||||
showLoader('Running Automatic (Vector first)...');
|
showLoader('Running Automatic (Vector first)...');
|
||||||
const vectorResultBytes = await performSmartCompression(arrayBuffer, smartSettings);
|
const vectorResultBytes = await performSmartCompression(
|
||||||
|
arrayBuffer,
|
||||||
|
smartSettings
|
||||||
|
);
|
||||||
|
|
||||||
if (vectorResultBytes.length < originalFile.size) {
|
if (vectorResultBytes.length < originalFile.size) {
|
||||||
resultBytes = vectorResultBytes;
|
resultBytes = vectorResultBytes;
|
||||||
usedMethod = 'Vector (Automatic)';
|
usedMethod = 'Vector (Automatic)';
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
showAlert('Vector failed to reduce size. Trying Photon...', 'info');
|
||||||
showAlert('Vector failed to reduce size. Trying Photon...', 'info', 3000);
|
|
||||||
showLoader('Running Automatic (Photon fallback)...');
|
showLoader('Running Automatic (Photon fallback)...');
|
||||||
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
|
resultBytes = await performLegacyCompression(
|
||||||
|
arrayBuffer,
|
||||||
|
legacySettings
|
||||||
|
);
|
||||||
usedMethod = 'Photon (Automatic)';
|
usedMethod = 'Photon (Automatic)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +310,8 @@ export async function compress() {
|
|||||||
const originalSize = formatBytes(originalFile.size);
|
const originalSize = formatBytes(originalFile.size);
|
||||||
const compressedSize = formatBytes(resultBytes.length);
|
const compressedSize = formatBytes(resultBytes.length);
|
||||||
const savings = originalFile.size - resultBytes.length;
|
const savings = originalFile.size - resultBytes.length;
|
||||||
const savingsPercent = savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
const savingsPercent =
|
||||||
|
savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
|
||||||
|
|
||||||
if (savings > 0) {
|
if (savings > 0) {
|
||||||
showAlert(
|
showAlert(
|
||||||
@@ -267,12 +329,15 @@ export async function compress() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadFile(new Blob([resultBytes], { type: 'application/pdf' }), 'compressed-final.pdf');
|
downloadFile(
|
||||||
|
new Blob([resultBytes], { type: 'application/pdf' }),
|
||||||
|
'compressed-final.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
showAlert(
|
||||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
|
'Error',
|
||||||
showAlert('Error', `An error occurred during compression. Error: ${e.message}`, 'error');
|
`An error occurred during compression. Error: ${e.message}`
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-29
@@ -1,11 +1,10 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import Cropper from "cropperjs";
|
import Cropper from 'cropperjs';
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
|
|
||||||
// --- Global State for the Cropper Tool ---
|
// --- Global State for the Cropper Tool ---
|
||||||
const cropperState = {
|
const cropperState = {
|
||||||
pdfDoc: null,
|
pdfDoc: null,
|
||||||
@@ -88,7 +87,7 @@ async function displayPageAsImage(num: any) {
|
|||||||
showAlert('Ready', 'Please select an area to crop.');
|
showAlert('Ready', 'Please select an area to crop.');
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error rendering page:", error);
|
console.error('Error rendering page:', error);
|
||||||
showAlert('Error', 'Failed to render page.');
|
showAlert('Error', 'Failed to render page.');
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
@@ -110,14 +109,17 @@ async function changePage(offset: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updatePageInfo() {
|
function updatePageInfo() {
|
||||||
document.getElementById('page-info').textContent = `Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
document.getElementById('page-info').textContent =
|
||||||
|
`Page ${cropperState.currentPageNum} of ${cropperState.pdfDoc.numPages}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function enableControls() {
|
function enableControls() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('prev-page').disabled = cropperState.currentPageNum <= 1;
|
document.getElementById('prev-page').disabled =
|
||||||
|
cropperState.currentPageNum <= 1;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('next-page').disabled = cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
|
document.getElementById('next-page').disabled =
|
||||||
|
cropperState.currentPageNum >= cropperState.pdfDoc.numPages;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
||||||
document.getElementById('crop-button').disabled = false;
|
document.getElementById('crop-button').disabled = false;
|
||||||
}
|
}
|
||||||
@@ -176,7 +178,9 @@ async function performFlatteningCrop(cropData: any) {
|
|||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
// Load the original PDF with pdf-lib to copy un-cropped pages from
|
||||||
const sourcePdfDocForCopying = await PDFLibDocument.load(cropperState.originalPdfBytes);
|
const sourcePdfDocForCopying = await PDFLibDocument.load(
|
||||||
|
cropperState.originalPdfBytes
|
||||||
|
);
|
||||||
const totalPages = cropperState.pdfDoc.numPages;
|
const totalPages = cropperState.pdfDoc.numPages;
|
||||||
|
|
||||||
for (let i = 0; i < totalPages; i++) {
|
for (let i = 0; i < totalPages; i++) {
|
||||||
@@ -203,24 +207,37 @@ async function performFlatteningCrop(cropData: any) {
|
|||||||
|
|
||||||
finalCtx.drawImage(
|
finalCtx.drawImage(
|
||||||
tempCanvas,
|
tempCanvas,
|
||||||
tempCanvas.width * crop.x, tempCanvas.height * crop.y,
|
tempCanvas.width * crop.x,
|
||||||
finalWidth, finalHeight,
|
tempCanvas.height * crop.y,
|
||||||
0, 0, finalWidth, finalHeight
|
finalWidth,
|
||||||
|
finalHeight,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
finalWidth,
|
||||||
|
finalHeight
|
||||||
);
|
);
|
||||||
|
|
||||||
const pngBytes = await new Promise(res => finalCanvas.toBlob(blob => blob.arrayBuffer().then(res), 'image/png'));
|
const pngBytes = await new Promise((res) =>
|
||||||
|
finalCanvas.toBlob((blob) => blob.arrayBuffer().then(res), 'image/png')
|
||||||
|
);
|
||||||
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
const embeddedImage = await newPdfDoc.embedPng(pngBytes as ArrayBuffer);
|
||||||
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
const newPage = newPdfDoc.addPage([finalWidth, finalHeight]);
|
||||||
newPage.drawImage(embeddedImage, { x: 0, y: 0, width: finalWidth, height: finalHeight });
|
newPage.drawImage(embeddedImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: finalWidth,
|
||||||
|
height: finalHeight,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [i]);
|
const [copiedPage] = await newPdfDoc.copyPages(sourcePdfDocForCopying, [
|
||||||
|
i,
|
||||||
|
]);
|
||||||
newPdfDoc.addPage(copiedPage);
|
newPdfDoc.addPage(copiedPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newPdfDoc;
|
return newPdfDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function setupCropperTool() {
|
export async function setupCropperTool() {
|
||||||
if (state.files.length === 0) return;
|
if (state.files.length === 0) return;
|
||||||
|
|
||||||
@@ -243,21 +260,30 @@ export async function setupCropperTool() {
|
|||||||
|
|
||||||
await displayPageAsImage(cropperState.currentPageNum);
|
await displayPageAsImage(cropperState.currentPageNum);
|
||||||
|
|
||||||
document.getElementById('prev-page').addEventListener('click', () => changePage(-1));
|
document
|
||||||
document.getElementById('next-page').addEventListener('click', () => changePage(1));
|
.getElementById('prev-page')
|
||||||
|
.addEventListener('click', () => changePage(-1));
|
||||||
|
document
|
||||||
|
.getElementById('next-page')
|
||||||
|
.addEventListener('click', () => changePage(1));
|
||||||
|
|
||||||
document.getElementById('crop-button').addEventListener('click', async () => {
|
document
|
||||||
|
.getElementById('crop-button')
|
||||||
|
.addEventListener('click', async () => {
|
||||||
// Get the last known crop from the active page before processing
|
// Get the last known crop from the active page before processing
|
||||||
saveCurrentCrop();
|
saveCurrentCrop();
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
const isDestructive = (
|
||||||
const isDestructive = document.getElementById('destructive-crop-toggle').checked;
|
document.getElementById('destructive-crop-toggle') as HTMLInputElement
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
).checked;
|
||||||
const isApplyToAll = document.getElementById('apply-to-all-toggle').checked;
|
const isApplyToAll = (
|
||||||
|
document.getElementById('apply-to-all-toggle') as HTMLInputElement
|
||||||
|
).checked;
|
||||||
|
|
||||||
let finalCropData = {};
|
let finalCropData = {};
|
||||||
if (isApplyToAll) {
|
if (isApplyToAll) {
|
||||||
const currentCrop = cropperState.pageCrops[cropperState.currentPageNum];
|
const currentCrop =
|
||||||
|
cropperState.pageCrops[cropperState.currentPageNum];
|
||||||
if (!currentCrop) {
|
if (!currentCrop) {
|
||||||
showAlert('No Crop Area', 'Please select an area to crop first.');
|
showAlert('No Crop Area', 'Please select an area to crop first.');
|
||||||
return;
|
return;
|
||||||
@@ -268,14 +294,20 @@ export async function setupCropperTool() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If not applying to all, only process pages with saved crops
|
// If not applying to all, only process pages with saved crops
|
||||||
finalCropData = Object.keys(cropperState.pageCrops).reduce((obj, key) => {
|
finalCropData = Object.keys(cropperState.pageCrops).reduce(
|
||||||
|
(obj, key) => {
|
||||||
obj[key] = cropperState.pageCrops[key];
|
obj[key] = cropperState.pageCrops[key];
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(finalCropData).length === 0) {
|
if (Object.keys(finalCropData).length === 0) {
|
||||||
showAlert('No Crop Area', 'Please select an area on at least one page to crop.');
|
showAlert(
|
||||||
|
'No Crop Area',
|
||||||
|
'Please select an area on at least one page to crop.'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,13 +319,20 @@ export async function setupCropperTool() {
|
|||||||
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
const newPdfDoc = await performFlatteningCrop(finalCropData);
|
||||||
finalPdfBytes = await newPdfDoc.save();
|
finalPdfBytes = await newPdfDoc.save();
|
||||||
} else {
|
} else {
|
||||||
const pdfToModify = await PDFLibDocument.load(cropperState.originalPdfBytes);
|
const pdfToModify = await PDFLibDocument.load(
|
||||||
|
cropperState.originalPdfBytes
|
||||||
|
);
|
||||||
await performMetadataCrop(pdfToModify, finalCropData);
|
await performMetadataCrop(pdfToModify, finalCropData);
|
||||||
finalPdfBytes = await pdfToModify.save();
|
finalPdfBytes = await pdfToModify.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = isDestructive ? 'flattened_crop.pdf' : 'standard_crop.pdf';
|
const fileName = isDestructive
|
||||||
downloadFile(new Blob([finalPdfBytes], { type: 'application/pdf' }), fileName);
|
? 'flattened_crop.pdf'
|
||||||
|
: 'standard_crop.pdf';
|
||||||
|
downloadFile(
|
||||||
|
new Blob([finalPdfBytes], { type: 'application/pdf' }),
|
||||||
|
fileName
|
||||||
|
);
|
||||||
showAlert('Success', 'Crop complete! Your download has started.');
|
showAlert('Success', 'Crop complete! Your download has started.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -303,7 +342,7 @@ export async function setupCropperTool() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error setting up cropper tool:", error);
|
console.error('Error setting up cropper tool:', error);
|
||||||
showAlert('Error', 'Failed to load PDF for cropping.');
|
showAlert('Error', 'Failed to load PDF for cropping.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+84
-44
@@ -1,65 +1,105 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
initializeQpdf,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
|
||||||
import blobStream from 'blob-stream';
|
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
|
||||||
|
|
||||||
export async function decrypt() {
|
export async function decrypt() {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const password = (
|
||||||
const password = document.getElementById('password-input').value;
|
document.getElementById('password-input') as HTMLInputElement
|
||||||
if (!password.trim()) {
|
)?.value;
|
||||||
|
|
||||||
|
if (!password) {
|
||||||
showAlert('Input Required', 'Please enter the PDF password.');
|
showAlert('Input Required', 'Please enter the PDF password.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputPath = '/input.pdf';
|
||||||
|
const outputPath = '/output.pdf';
|
||||||
|
let qpdf: any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Preparing to process...');
|
showLoader('Initializing decryption...');
|
||||||
const pdfData = await readFileAsArrayBuffer(file);
|
qpdf = await initializeQpdf();
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
|
||||||
const pdf = await pdfjsLib.getDocument({ data: pdfData, password: password }).promise;
|
|
||||||
const numPages = pdf.numPages;
|
|
||||||
const pageImages = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= numPages; i++) {
|
showLoader('Reading encrypted PDF...');
|
||||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
const page = await pdf.getPage(i);
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
|
||||||
const canvas = document.createElement('canvas');
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
canvas.height = viewport.height;
|
showLoader('Decrypting PDF...');
|
||||||
canvas.width = viewport.width;
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
const args = [inputPath, '--password=' + password, '--decrypt', outputPath];
|
||||||
pageImages.push({
|
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
try {
|
||||||
width: viewport.width,
|
qpdf.callMain(args);
|
||||||
height: viewport.height
|
} catch (qpdfError: any) {
|
||||||
});
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
|
||||||
|
if (
|
||||||
|
qpdfError.message?.includes('invalid password') ||
|
||||||
|
qpdfError.message?.includes('password')
|
||||||
|
) {
|
||||||
|
throw new Error('INVALID_PASSWORD');
|
||||||
|
}
|
||||||
|
throw qpdfError;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent = 'Building unlocked PDF...';
|
showLoader('Preparing download...');
|
||||||
const doc = new PDFDocument({ size: [pageImages[0].width, pageImages[0].height] });
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
const stream = doc.pipe(blobStream());
|
|
||||||
for (let i = 0; i < pageImages.length; i++) {
|
|
||||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
|
||||||
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
|
|
||||||
}
|
|
||||||
doc.end();
|
|
||||||
|
|
||||||
stream.on('finish', function () {
|
if (outputFile.length === 0) {
|
||||||
const blob = stream.toBlob('application/pdf');
|
throw new Error('Decryption resulted in an empty file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
downloadFile(blob, `unlocked-${file.name}`);
|
downloadFile(blob, `unlocked-${file.name}`);
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Success', 'Decryption complete! Your download has started.');
|
showAlert(
|
||||||
});
|
'Success',
|
||||||
} catch (error) {
|
'PDF decrypted successfully! Your download has started.'
|
||||||
console.error("Error during PDF decryption:", error);
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error during PDF decryption:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
if (error.name === 'PasswordException') {
|
|
||||||
showAlert('Incorrect Password', 'The password you entered is incorrect.');
|
if (error.message === 'INVALID_PASSWORD') {
|
||||||
|
showAlert(
|
||||||
|
'Incorrect Password',
|
||||||
|
'The password you entered is incorrect. Please try again.'
|
||||||
|
);
|
||||||
|
} else if (error.message?.includes('password')) {
|
||||||
|
showAlert(
|
||||||
|
'Password Error',
|
||||||
|
'Unable to decrypt the PDF with the provided password.'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
showAlert(
|
||||||
|
'Decryption Failed',
|
||||||
|
`An error occurred: ${error.message || 'The password you entered is wrong or the file is corrupted.'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (qpdf?.FS) {
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(inputPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to unlink input file:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(outputPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to unlink output file:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,14 @@ export async function deletePages() {
|
|||||||
const trimmedRange = range.trim();
|
const trimmedRange = range.trim();
|
||||||
if (trimmedRange.includes('-')) {
|
if (trimmedRange.includes('-')) {
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
|
for (let i = start; i <= end; i++) indicesToDelete.add(i - 1);
|
||||||
} else {
|
} else {
|
||||||
const pageNum = Number(trimmedRange);
|
const pageNum = Number(trimmedRange);
|
||||||
@@ -41,13 +48,19 @@ export async function deletePages() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const indicesToKeep = Array.from({ length: totalPages }, (_, i) => i).filter(index => !indicesToDelete.has(index));
|
const indicesToKeep = Array.from(
|
||||||
|
{ length: totalPages },
|
||||||
|
(_, i) => i
|
||||||
|
).filter((index) => !indicesToDelete.has(index));
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'deleted-pages.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'deleted-pages.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not delete pages.');
|
showAlert('Error', 'Could not delete pages.');
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import Sortable from 'sortablejs'
|
import Sortable from 'sortablejs';
|
||||||
import {icons, createIcons} from "lucide";
|
import { icons, createIcons } from 'lucide';
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
const duplicateOrganizeState = {
|
const duplicateOrganizeState = {
|
||||||
sortableInstances: {}
|
sortableInstances: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function initializePageGridSortable() {
|
function initializePageGridSortable() {
|
||||||
@@ -32,7 +32,7 @@ function initializePageGridSortable() {
|
|||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: any) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,9 @@ function attachEventListeners(element: any) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Duplicate button listener
|
// Duplicate button listener
|
||||||
element.querySelector('.duplicate-btn').addEventListener('click', (e: any) => {
|
element
|
||||||
|
.querySelector('.duplicate-btn')
|
||||||
|
.addEventListener('click', (e: any) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const clone = element.cloneNode(true);
|
const clone = element.cloneNode(true);
|
||||||
element.after(clone);
|
element.after(clone);
|
||||||
@@ -68,7 +70,10 @@ function attachEventListeners(element: any) {
|
|||||||
renumberPages();
|
renumberPages();
|
||||||
initializePageGridSortable();
|
initializePageGridSortable();
|
||||||
} else {
|
} else {
|
||||||
showAlert('Cannot Delete', 'You cannot delete the last page of the document.');
|
showAlert(
|
||||||
|
'Cannot Delete',
|
||||||
|
'You cannot delete the last page of the document.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,15 +95,18 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport }).promise;
|
await page.render({ canvasContext: canvas.getContext('2d'), viewport })
|
||||||
|
.promise;
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
wrapper.className =
|
||||||
|
'page-thumbnail relative cursor-move flex flex-col items-center gap-2';
|
||||||
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
// @ts-expect-error TS(2322) FIXME: Type 'number' is not assignable to type 'string'.
|
||||||
wrapper.dataset.originalPageIndex = i - 1;
|
wrapper.dataset.originalPageIndex = i - 1;
|
||||||
|
|
||||||
const imgContainer = document.createElement('div');
|
const imgContainer = document.createElement('div');
|
||||||
imgContainer.className = 'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
imgContainer.className =
|
||||||
|
'w-full h-36 bg-gray-900 rounded-lg flex items-center justify-center overflow-hidden border-2 border-gray-600';
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = canvas.toDataURL();
|
img.src = canvas.toDataURL();
|
||||||
@@ -106,14 +114,16 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
imgContainer.appendChild(img);
|
imgContainer.appendChild(img);
|
||||||
|
|
||||||
const pageNumberSpan = document.createElement('span');
|
const pageNumberSpan = document.createElement('span');
|
||||||
pageNumberSpan.className = 'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
pageNumberSpan.className =
|
||||||
|
'page-number absolute top-1 left-1 bg-gray-900 bg-opacity-75 text-white text-xs rounded-full px-2 py-1';
|
||||||
pageNumberSpan.textContent = i.toString();
|
pageNumberSpan.textContent = i.toString();
|
||||||
|
|
||||||
const controlsDiv = document.createElement('div');
|
const controlsDiv = document.createElement('div');
|
||||||
controlsDiv.className = 'flex items-center justify-center gap-4';
|
controlsDiv.className = 'flex items-center justify-center gap-4';
|
||||||
|
|
||||||
const duplicateBtn = document.createElement('button');
|
const duplicateBtn = document.createElement('button');
|
||||||
duplicateBtn.className = 'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
duplicateBtn.className =
|
||||||
|
'duplicate-btn bg-green-600 hover:bg-green-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||||
duplicateBtn.title = 'Duplicate Page';
|
duplicateBtn.title = 'Duplicate Page';
|
||||||
const duplicateIcon = document.createElement('i');
|
const duplicateIcon = document.createElement('i');
|
||||||
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
duplicateIcon.setAttribute('data-lucide', 'copy-plus');
|
||||||
@@ -121,7 +131,8 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
duplicateBtn.appendChild(duplicateIcon);
|
duplicateBtn.appendChild(duplicateIcon);
|
||||||
|
|
||||||
const deleteBtn = document.createElement('button');
|
const deleteBtn = document.createElement('button');
|
||||||
deleteBtn.className = 'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
deleteBtn.className =
|
||||||
|
'delete-btn bg-red-600 hover:bg-red-700 text-white rounded-full w-8 h-8 flex items-center justify-center';
|
||||||
deleteBtn.title = 'Delete Page';
|
deleteBtn.title = 'Delete Page';
|
||||||
const deleteIcon = document.createElement('i');
|
const deleteIcon = document.createElement('i');
|
||||||
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
deleteIcon.setAttribute('data-lucide', 'x-circle');
|
||||||
@@ -139,22 +150,25 @@ export async function renderDuplicateOrganizeThumbnails() {
|
|||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function processAndSave() {
|
export async function processAndSave() {
|
||||||
showLoader('Building new PDF...');
|
showLoader('Building new PDF...');
|
||||||
try {
|
try {
|
||||||
const grid = document.getElementById('page-grid');
|
const grid = document.getElementById('page-grid');
|
||||||
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
const finalPageElements = grid.querySelectorAll('.page-thumbnail');
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
const finalIndices = Array.from(finalPageElements).map((el) =>
|
||||||
const finalIndices = Array.from(finalPageElements).map(el => parseInt(el.dataset.originalPageIndex));
|
parseInt((el as HTMLElement).dataset.originalPageIndex)
|
||||||
|
);
|
||||||
|
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
const copiedPages = await newPdfDoc.copyPages(state.pdfDoc, finalIndices);
|
||||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||||
|
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'organized.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to save the new PDF.');
|
showAlert('Error', 'Failed to save the new PDF.');
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { PDFName, PDFString } from "pdf-lib"
|
import { PDFName, PDFString } from 'pdf-lib';
|
||||||
|
|
||||||
|
|
||||||
export async function editMetadata() {
|
export async function editMetadata() {
|
||||||
showLoader('Updating metadata...');
|
showLoader('Updating metadata...');
|
||||||
@@ -20,7 +19,12 @@ export async function editMetadata() {
|
|||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const keywords = document.getElementById('meta-keywords').value;
|
const keywords = document.getElementById('meta-keywords').value;
|
||||||
state.pdfDoc.setKeywords(keywords.split(',').map((k: any) => k.trim()).filter(Boolean));
|
state.pdfDoc.setKeywords(
|
||||||
|
keywords
|
||||||
|
.split(',')
|
||||||
|
.map((k: any) => k.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const creationDate = document.getElementById('meta-creation-date').value;
|
const creationDate = document.getElementById('meta-creation-date').value;
|
||||||
@@ -37,9 +41,20 @@ export async function editMetadata() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const infoDict = state.pdfDoc.getInfoDict();
|
const infoDict = state.pdfDoc.getInfoDict();
|
||||||
const standardKeys = new Set(['Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', 'CreationDate', 'ModDate']);
|
const standardKeys = new Set([
|
||||||
|
'Title',
|
||||||
|
'Author',
|
||||||
|
'Subject',
|
||||||
|
'Keywords',
|
||||||
|
'Creator',
|
||||||
|
'Producer',
|
||||||
|
'CreationDate',
|
||||||
|
'ModDate',
|
||||||
|
]);
|
||||||
|
|
||||||
const allKeys = infoDict.keys().map((key: any) => key.asString().substring(1)); // Clean keys
|
const allKeys = infoDict
|
||||||
|
.keys()
|
||||||
|
.map((key: any) => key.asString().substring(1)); // Clean keys
|
||||||
|
|
||||||
allKeys.forEach((key: any) => {
|
allKeys.forEach((key: any) => {
|
||||||
if (!standardKeys.has(key)) {
|
if (!standardKeys.has(key)) {
|
||||||
@@ -62,11 +77,16 @@ export async function editMetadata() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-edited.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'metadata-edited.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not update metadata. Please check that date formats are correct.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Could not update metadata. Please check that date formats are correct.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-55
@@ -1,73 +1,113 @@
|
|||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
initializeQpdf,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import PDFDocument from 'pdfkit/js/pdfkit.standalone';
|
|
||||||
import blobStream from 'blob-stream';
|
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
|
||||||
|
|
||||||
export async function encrypt() {
|
export async function encrypt() {
|
||||||
const file = state.files[0];
|
const file = state.files[0];
|
||||||
const password = (document.getElementById('password-input') as HTMLInputElement).value;
|
const userPassword =
|
||||||
if (!password.trim()) {
|
(document.getElementById('user-password-input') as HTMLInputElement)
|
||||||
showAlert('Input Required', 'Please enter a password.');
|
?.value || '';
|
||||||
|
const ownerPasswordInput =
|
||||||
|
(document.getElementById('owner-password-input') as HTMLInputElement)
|
||||||
|
?.value || '';
|
||||||
|
|
||||||
|
if (!userPassword) {
|
||||||
|
showAlert('Input Required', 'Please enter a user password.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ownerPassword = ownerPasswordInput || userPassword;
|
||||||
|
const hasDistinctOwnerPassword = ownerPasswordInput !== '';
|
||||||
|
|
||||||
|
const inputPath = '/input.pdf';
|
||||||
|
const outputPath = '/output.pdf';
|
||||||
|
let qpdf: any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoader('Preparing to process...');
|
showLoader('Initializing encryption...');
|
||||||
const pdfData = await readFileAsArrayBuffer(file);
|
qpdf = await initializeQpdf();
|
||||||
const pdf = await pdfjsLib.getDocument({ data: pdfData as ArrayBuffer }).promise;
|
|
||||||
const numPages = pdf.numPages;
|
|
||||||
const pageImages = [];
|
|
||||||
|
|
||||||
for (let i = 1; i <= numPages; i++) {
|
showLoader('Reading PDF...');
|
||||||
document.getElementById('loader-text').textContent = `Processing page ${i} of ${numPages}...`;
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
const page = await pdf.getPage(i);
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
const viewport = page.getViewport({ scale: 2.0 });
|
|
||||||
const canvas = document.createElement('canvas');
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
canvas.height = viewport.height;
|
showLoader('Encrypting PDF with 256-bit AES...');
|
||||||
canvas.width = viewport.width;
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
const args = [inputPath, '--encrypt', userPassword, ownerPassword, '256'];
|
||||||
pageImages.push({
|
|
||||||
data: canvas.toDataURL('image/jpeg', 0.8),
|
// Only add restrictions if a distinct owner password was provided
|
||||||
width: viewport.width,
|
if (hasDistinctOwnerPassword) {
|
||||||
height: viewport.height
|
args.push(
|
||||||
});
|
'--modify=none',
|
||||||
|
'--extract=n',
|
||||||
|
'--print=none',
|
||||||
|
'--accessibility=n',
|
||||||
|
'--annotate=n',
|
||||||
|
'--assemble=n',
|
||||||
|
'--form=n',
|
||||||
|
'--modify-other=n'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('loader-text').textContent = 'Encrypting and building PDF...';
|
args.push('--', outputPath);
|
||||||
const doc = new PDFDocument({
|
|
||||||
size: [pageImages[0].width, pageImages[0].height],
|
|
||||||
pdfVersion: '1.7ext3', // Use 256-bit AES encryption
|
|
||||||
userPassword: password,
|
|
||||||
ownerPassword: password,
|
|
||||||
permissions: {
|
|
||||||
printing: 'highResolution',
|
|
||||||
modifying: false,
|
|
||||||
copying: false,
|
|
||||||
annotating: false,
|
|
||||||
fillingForms: false,
|
|
||||||
contentAccessibility: true,
|
|
||||||
documentAssembly: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const stream = doc.pipe(blobStream());
|
|
||||||
for (let i = 0; i < pageImages.length; i++) {
|
|
||||||
if (i > 0) doc.addPage({ size: [pageImages[i].width, pageImages[i].height] });
|
|
||||||
doc.image(pageImages[i].data, 0, 0, { width: pageImages[i].width, height: pageImages[i].height });
|
|
||||||
}
|
|
||||||
doc.end();
|
|
||||||
|
|
||||||
stream.on('finish', function () {
|
try {
|
||||||
const blob = stream.toBlob('application/pdf');
|
qpdf.callMain(args);
|
||||||
|
} catch (qpdfError: any) {
|
||||||
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
throw new Error(
|
||||||
|
'Encryption failed: ' + (qpdfError.message || 'Unknown error')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Preparing download...');
|
||||||
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
|
||||||
|
if (!outputFile || outputFile.length === 0) {
|
||||||
|
throw new Error('Encryption resulted in an empty file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
downloadFile(blob, `encrypted-${file.name}`);
|
downloadFile(blob, `encrypted-${file.name}`);
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Success', 'Encryption complete! Your download has started.');
|
|
||||||
});
|
let successMessage = 'PDF encrypted successfully with 256-bit AES!';
|
||||||
} catch (error) {
|
if (!hasDistinctOwnerPassword) {
|
||||||
console.error("Error during PDF encryption:", error);
|
successMessage +=
|
||||||
|
' Note: Without a separate owner password, the PDF has no usage restrictions.';
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert('Success', successMessage);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error during PDF encryption:', error);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
showAlert('Error', 'An error occurred. The PDF might be corrupted.');
|
showAlert(
|
||||||
|
'Encryption Failed',
|
||||||
|
`An error occurred: ${error.message || 'The PDF might be corrupted.'}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (qpdf?.FS) {
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(inputPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to unlink input file:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(outputPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to unlink output file:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,14 @@ export async function extractPages() {
|
|||||||
const trimmedRange = range.trim();
|
const trimmedRange = range.trim();
|
||||||
if (trimmedRange.includes('-')) {
|
if (trimmedRange.includes('-')) {
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
|
for (let i = start; i <= end; i++) indicesToExtract.add(i - 1);
|
||||||
} else {
|
} else {
|
||||||
const pageNum = Number(trimmedRange);
|
const pageNum = Number(trimmedRange);
|
||||||
@@ -43,7 +50,9 @@ export async function extractPages() {
|
|||||||
|
|
||||||
for (const index of sortedIndices) {
|
for (const index of sortedIndices) {
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [index as number]);
|
const [copiedPage] = await newPdf.copyPages(state.pdfDoc, [
|
||||||
|
index as number,
|
||||||
|
]);
|
||||||
newPdf.addPage(copiedPage);
|
newPdf.addPage(copiedPage);
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
// @ts-expect-error TS(2365) FIXME: Operator '+' cannot be applied to types 'unknown' ... Remove this comment to see the full error message
|
||||||
|
|||||||
@@ -9,21 +9,30 @@ export function setupFixDimensionsUI() {
|
|||||||
const customSizeWrapper = document.getElementById('custom-size-wrapper');
|
const customSizeWrapper = document.getElementById('custom-size-wrapper');
|
||||||
if (targetSizeSelect && customSizeWrapper) {
|
if (targetSizeSelect && customSizeWrapper) {
|
||||||
targetSizeSelect.addEventListener('change', () => {
|
targetSizeSelect.addEventListener('change', () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
customSizeWrapper.classList.toggle(
|
||||||
customSizeWrapper.classList.toggle('hidden', targetSizeSelect.value !== 'Custom');
|
'hidden',
|
||||||
|
(targetSizeSelect as HTMLSelectElement).value !== 'Custom'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fixDimensions() {
|
export async function fixDimensions() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const targetSizeKey = (
|
||||||
const targetSizeKey = document.getElementById('target-size').value;
|
document.getElementById('target-size') as HTMLSelectElement
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
).value;
|
||||||
const orientation = document.getElementById('orientation').value;
|
const orientation = (
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
document.getElementById('orientation') as HTMLSelectElement
|
||||||
const scalingMode = document.querySelector('input[name="scaling-mode"]:checked').value;
|
).value;
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
|
||||||
const backgroundColor = hexToRgb(document.getElementById('background-color').value);
|
const scalingMode = (
|
||||||
|
document.querySelector(
|
||||||
|
'input[name="scaling-mode"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
const backgroundColor = hexToRgb(
|
||||||
|
(document.getElementById('background-color') as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
|
||||||
showLoader('Standardizing pages...');
|
showLoader('Standardizing pages...');
|
||||||
try {
|
try {
|
||||||
@@ -39,7 +48,8 @@ export async function fixDimensions() {
|
|||||||
if (units === 'in') {
|
if (units === 'in') {
|
||||||
targetWidth = width * 72;
|
targetWidth = width * 72;
|
||||||
targetHeight = height * 72;
|
targetHeight = height * 72;
|
||||||
} else { // mm
|
} else {
|
||||||
|
// mm
|
||||||
targetWidth = width * (72 / 25.4);
|
targetWidth = width * (72 / 25.4);
|
||||||
targetHeight = height * (72 / 25.4);
|
targetHeight = height * (72 / 25.4);
|
||||||
}
|
}
|
||||||
@@ -61,11 +71,20 @@ export async function fixDimensions() {
|
|||||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||||
|
|
||||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||||
newPage.drawRectangle({ x: 0, y: 0, width: targetWidth, height: targetHeight, color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b) });
|
newPage.drawRectangle({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: targetWidth,
|
||||||
|
height: targetHeight,
|
||||||
|
color: rgb(backgroundColor.r, backgroundColor.g, backgroundColor.b),
|
||||||
|
});
|
||||||
|
|
||||||
const scaleX = targetWidth / sourceWidth;
|
const scaleX = targetWidth / sourceWidth;
|
||||||
const scaleY = targetHeight / sourceHeight;
|
const scaleY = targetHeight / sourceHeight;
|
||||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
const scale =
|
||||||
|
scalingMode === 'fit'
|
||||||
|
? Math.min(scaleX, scaleY)
|
||||||
|
: Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
const scaledWidth = sourceWidth * scale;
|
const scaledWidth = sourceWidth * scale;
|
||||||
const scaledHeight = sourceHeight * scale;
|
const scaledHeight = sourceHeight * scale;
|
||||||
@@ -73,12 +92,19 @@ export async function fixDimensions() {
|
|||||||
const x = (targetWidth - scaledWidth) / 2;
|
const x = (targetWidth - scaledWidth) / 2;
|
||||||
const y = (targetHeight - scaledHeight) / 2;
|
const y = (targetHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
newPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
newPage.drawPage(embeddedPage, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: scaledWidth,
|
||||||
|
height: scaledHeight,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
const newPdfBytes = await newDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'standardized.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'standardized.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while standardizing pages.');
|
showAlert('Error', 'An error occurred while standardizing pages.');
|
||||||
|
|||||||
+14
-4
@@ -2,6 +2,11 @@ import { showLoader, hideLoader, showAlert } from '../ui.js';
|
|||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
|
export function flattenFormsInDoc(pdfDoc) {
|
||||||
|
const form = pdfDoc.getForm();
|
||||||
|
form.flatten();
|
||||||
|
}
|
||||||
|
|
||||||
export async function flatten() {
|
export async function flatten() {
|
||||||
if (!state.pdfDoc) {
|
if (!state.pdfDoc) {
|
||||||
showAlert('Error', 'PDF not loaded.');
|
showAlert('Error', 'PDF not loaded.');
|
||||||
@@ -9,15 +14,20 @@ export async function flatten() {
|
|||||||
}
|
}
|
||||||
showLoader('Flattening PDF...');
|
showLoader('Flattening PDF...');
|
||||||
try {
|
try {
|
||||||
const form = state.pdfDoc.getForm();
|
flattenFormsInDoc(state.pdfDoc);
|
||||||
form.flatten();
|
|
||||||
|
|
||||||
const flattenedBytes = await state.pdfDoc.save();
|
const flattenedBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([flattenedBytes], { type: 'application/pdf' }), 'flattened.pdf');
|
downloadFile(
|
||||||
|
new Blob([flattenedBytes], { type: 'application/pdf' }),
|
||||||
|
'flattened.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
if (e.message.includes('getForm')) {
|
if (e.message.includes('getForm')) {
|
||||||
showAlert('No Form Found', 'This PDF does not contain any form fields to flatten.');
|
showAlert(
|
||||||
|
'No Form Found',
|
||||||
|
'This PDF does not contain any form fields to flatten.'
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
showAlert('Error', 'Could not flatten the PDF.');
|
showAlert('Error', 'Could not flatten the PDF.');
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-29
@@ -9,7 +9,7 @@ import {
|
|||||||
PDFDropdown,
|
PDFDropdown,
|
||||||
PDFButton,
|
PDFButton,
|
||||||
PDFSignature,
|
PDFSignature,
|
||||||
PDFOptionList
|
PDFOptionList,
|
||||||
} from 'pdf-lib';
|
} from 'pdf-lib';
|
||||||
|
|
||||||
import * as pdfjsLib from 'pdfjs-dist';
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
@@ -55,9 +55,11 @@ async function renderPage() {
|
|||||||
canvas.style.transformOrigin = 'top left';
|
canvas.style.transformOrigin = 'top left';
|
||||||
canvas.style.transform = `scale(${formState.scale})`;
|
canvas.style.transform = `scale(${formState.scale})`;
|
||||||
|
|
||||||
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), { ignoreEncryption: true });
|
const tempPdfDoc = await PDFLibDocument.load(await state.pdfDoc.save(), {
|
||||||
|
ignoreEncryption: true,
|
||||||
|
});
|
||||||
const form = tempPdfDoc.getForm();
|
const form = tempPdfDoc.getForm();
|
||||||
Object.keys(fieldValues).forEach(fieldName => {
|
Object.keys(fieldValues).forEach((fieldName) => {
|
||||||
try {
|
try {
|
||||||
const field = form.getField(fieldName);
|
const field = form.getField(fieldName);
|
||||||
if (!field) return;
|
if (!field) return;
|
||||||
@@ -76,7 +78,9 @@ async function renderPage() {
|
|||||||
field.select(fieldValues[fieldName]);
|
field.select(fieldValues[fieldName]);
|
||||||
} else if (field instanceof PDFOptionList) {
|
} else if (field instanceof PDFOptionList) {
|
||||||
if (Array.isArray(fieldValues[fieldName])) {
|
if (Array.isArray(fieldValues[fieldName])) {
|
||||||
(fieldValues[fieldName] as any[]).forEach(option => field.select(option));
|
(fieldValues[fieldName] as any[]).forEach((option) =>
|
||||||
|
field.select(option)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -85,12 +89,13 @@ async function renderPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tempPdfBytes = await tempPdfDoc.save();
|
const tempPdfBytes = await tempPdfDoc.save();
|
||||||
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes }).promise;
|
const tempPdfJsDoc = await pdfjsLib.getDocument({ data: tempPdfBytes })
|
||||||
|
.promise;
|
||||||
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
const tempPage = await tempPdfJsDoc.getPage(currentPageNum);
|
||||||
|
|
||||||
await tempPage.render({
|
await tempPage.render({
|
||||||
canvasContext: context,
|
canvasContext: context,
|
||||||
viewport: viewport
|
viewport: viewport,
|
||||||
} as any).promise;
|
} as any).promise;
|
||||||
|
|
||||||
const currentPageDisplay = document.getElementById('current-page-display');
|
const currentPageDisplay = document.getElementById('current-page-display');
|
||||||
@@ -98,8 +103,10 @@ async function renderPage() {
|
|||||||
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
const prevPageBtn = document.getElementById('prev-page') as HTMLButtonElement;
|
||||||
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
|
const nextPageBtn = document.getElementById('next-page') as HTMLButtonElement;
|
||||||
|
|
||||||
if (currentPageDisplay) currentPageDisplay.textContent = String(currentPageNum);
|
if (currentPageDisplay)
|
||||||
if (totalPagesDisplay) totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
|
currentPageDisplay.textContent = String(currentPageNum);
|
||||||
|
if (totalPagesDisplay)
|
||||||
|
totalPagesDisplay.textContent = String(pdfJsDoc.numPages);
|
||||||
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
|
if (prevPageBtn) prevPageBtn.disabled = currentPageNum <= 1;
|
||||||
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
|
if (nextPageBtn) nextPageBtn.disabled = currentPageNum >= pdfJsDoc.numPages;
|
||||||
|
|
||||||
@@ -129,8 +136,8 @@ function handleFormChange(event: Event) {
|
|||||||
value = input.checked ? 'on' : 'off';
|
value = input.checked ? 'on' : 'off';
|
||||||
} else if (input instanceof HTMLSelectElement && input.multiple) {
|
} else if (input instanceof HTMLSelectElement && input.multiple) {
|
||||||
value = Array.from(input.options)
|
value = Array.from(input.options)
|
||||||
.filter(option => option.selected)
|
.filter((option) => option.selected)
|
||||||
.map(option => option.value);
|
.map((option) => option.value);
|
||||||
} else {
|
} else {
|
||||||
value = input.value;
|
value = input.value;
|
||||||
}
|
}
|
||||||
@@ -149,7 +156,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
const labelText = name.replace(/[_-]/g, ' ');
|
const labelText = name.replace(/[_-]/g, ' ');
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
|
wrapper.className =
|
||||||
|
'form-field-group p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
|
|
||||||
const label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
label.htmlFor = `field-${name}`;
|
label.htmlFor = `field-${name}`;
|
||||||
@@ -174,7 +182,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
input.id = `field-${name}`;
|
input.id = `field-${name}`;
|
||||||
input.name = name;
|
input.name = name;
|
||||||
input.value = fieldValues[name];
|
input.value = fieldValues[name];
|
||||||
input.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
input.className =
|
||||||
|
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||||
inputElement = input;
|
inputElement = input;
|
||||||
} else if (field instanceof PDFCheckBox) {
|
} else if (field instanceof PDFCheckBox) {
|
||||||
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
fieldValues[name] = field.isChecked() ? 'on' : 'off';
|
||||||
@@ -182,7 +191,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
input.type = 'checkbox';
|
input.type = 'checkbox';
|
||||||
input.id = `field-${name}`;
|
input.id = `field-${name}`;
|
||||||
input.name = name;
|
input.name = name;
|
||||||
input.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
input.className =
|
||||||
|
'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||||
input.checked = field.isChecked();
|
input.checked = field.isChecked();
|
||||||
inputElement = input;
|
inputElement = input;
|
||||||
} else if (field instanceof PDFRadioGroup) {
|
} else if (field instanceof PDFRadioGroup) {
|
||||||
@@ -197,7 +207,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
radio.type = 'radio';
|
radio.type = 'radio';
|
||||||
radio.name = name;
|
radio.name = name;
|
||||||
radio.value = opt;
|
radio.value = opt;
|
||||||
radio.className = 'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
radio.className =
|
||||||
|
'w-4 h-4 rounded text-indigo-600 bg-gray-700 border-gray-600 focus:ring-indigo-500';
|
||||||
if (opt === field.getSelected()) radio.checked = true;
|
if (opt === field.getSelected()) radio.checked = true;
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
@@ -216,7 +227,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
const select = document.createElement('select');
|
const select = document.createElement('select');
|
||||||
select.id = `field-${name}`;
|
select.id = `field-${name}`;
|
||||||
select.name = name;
|
select.name = name;
|
||||||
select.className = 'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
select.className =
|
||||||
|
'w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5';
|
||||||
|
|
||||||
if (field instanceof PDFOptionList) {
|
if (field instanceof PDFOptionList) {
|
||||||
select.multiple = true;
|
select.multiple = true;
|
||||||
@@ -234,7 +246,8 @@ function createFormFieldHtml(field: any): HTMLElement {
|
|||||||
inputElement = select;
|
inputElement = select;
|
||||||
} else {
|
} else {
|
||||||
const unsupportedDiv = document.createElement('div');
|
const unsupportedDiv = document.createElement('div');
|
||||||
unsupportedDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
unsupportedDiv.className =
|
||||||
|
'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.className = 'text-sm text-gray-400';
|
p.className = 'text-sm text-gray-400';
|
||||||
if (field instanceof PDFSignature) {
|
if (field instanceof PDFSignature) {
|
||||||
@@ -273,7 +286,8 @@ export async function setupFormFiller() {
|
|||||||
formContainer.textContent = '';
|
formContainer.textContent = '';
|
||||||
|
|
||||||
if (fields.length === 0) {
|
if (fields.length === 0) {
|
||||||
formContainer.innerHTML = '<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
formContainer.innerHTML =
|
||||||
|
'<p class="text-center text-gray-400">This PDF contains no form fields.</p>';
|
||||||
processBtn.classList.add('hidden');
|
processBtn.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
fields.forEach((field: any) => {
|
fields.forEach((field: any) => {
|
||||||
@@ -283,7 +297,8 @@ export async function setupFormFiller() {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`Error processing field "${field.getName()}":`, e);
|
console.error(`Error processing field "${field.getName()}":`, e);
|
||||||
const errorDiv = document.createElement('div');
|
const errorDiv = document.createElement('div');
|
||||||
errorDiv.className = 'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
errorDiv.className =
|
||||||
|
'p-4 bg-gray-800 rounded-lg border border-gray-700';
|
||||||
// Sanitize error message display
|
// Sanitize error message display
|
||||||
const p1 = document.createElement('p');
|
const p1 = document.createElement('p');
|
||||||
p1.className = 'text-sm text-gray-500';
|
p1.className = 'text-sm text-gray-500';
|
||||||
@@ -311,19 +326,28 @@ export async function setupFormFiller() {
|
|||||||
const prevPageBtn = document.getElementById('prev-page');
|
const prevPageBtn = document.getElementById('prev-page');
|
||||||
const nextPageBtn = document.getElementById('next-page');
|
const nextPageBtn = document.getElementById('next-page');
|
||||||
|
|
||||||
if (zoomInBtn) zoomInBtn.addEventListener('click', () => setZoom(formState.scale + 0.25));
|
if (zoomInBtn)
|
||||||
if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => setZoom(Math.max(1, formState.scale - 0.25)));
|
zoomInBtn.addEventListener('click', () =>
|
||||||
if (prevPageBtn) prevPageBtn.addEventListener('click', () => changePage(-1));
|
setZoom(formState.scale + 0.25)
|
||||||
|
);
|
||||||
|
if (zoomOutBtn)
|
||||||
|
zoomOutBtn.addEventListener('click', () =>
|
||||||
|
setZoom(Math.max(1, formState.scale - 0.25))
|
||||||
|
);
|
||||||
|
if (prevPageBtn)
|
||||||
|
prevPageBtn.addEventListener('click', () => changePage(-1));
|
||||||
if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1));
|
if (nextPageBtn) nextPageBtn.addEventListener('click', () => changePage(1));
|
||||||
|
|
||||||
hideLoader();
|
hideLoader();
|
||||||
|
|
||||||
const formFillerOptions = document.getElementById('form-filler-options');
|
const formFillerOptions = document.getElementById('form-filler-options');
|
||||||
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
if (formFillerOptions) formFillerOptions.classList.remove('hidden');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Critical error setting up form filler:", e);
|
console.error('Critical error setting up form filler:', e);
|
||||||
showAlert('Error', 'Failed to read PDF form data. The file may be corrupt or not a valid form.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to read PDF form data. The file may be corrupt or not a valid form.'
|
||||||
|
);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,7 +357,7 @@ export async function processAndDownloadForm() {
|
|||||||
try {
|
try {
|
||||||
const form = state.pdfDoc.getForm();
|
const form = state.pdfDoc.getForm();
|
||||||
|
|
||||||
Object.keys(fieldValues).forEach(fieldName => {
|
Object.keys(fieldValues).forEach((fieldName) => {
|
||||||
try {
|
try {
|
||||||
const field = form.getField(fieldName);
|
const field = form.getField(fieldName);
|
||||||
const value = fieldValues[fieldName];
|
const value = fieldValues[fieldName];
|
||||||
@@ -352,19 +376,24 @@ export async function processAndDownloadForm() {
|
|||||||
field.select(value);
|
field.select(value);
|
||||||
} else if (field instanceof PDFOptionList) {
|
} else if (field instanceof PDFOptionList) {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach(option => field.select(option));
|
value.forEach((option) => field.select(option));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error processing field "${fieldName}" during download:`, e);
|
console.error(
|
||||||
|
`Error processing field "${fieldName}" during download:`,
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'filled-form.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'filled-form.pdf'
|
||||||
|
);
|
||||||
|
|
||||||
showAlert('Success', 'Form has been filled and downloaded.');
|
showAlert('Success', 'Form has been filled and downloaded.');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to save the filled form.');
|
showAlert('Error', 'Failed to save the filled form.');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -16,20 +15,33 @@ export async function heicToPdf() {
|
|||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
const conversionResult = await heic2any({
|
const conversionResult = await heic2any({
|
||||||
blob: file,
|
blob: file,
|
||||||
toType: "image/png",
|
toType: 'image/png',
|
||||||
});
|
});
|
||||||
const pngBlob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult;
|
const pngBlob = Array.isArray(conversionResult)
|
||||||
|
? conversionResult[0]
|
||||||
|
: conversionResult;
|
||||||
const pngBytes = await pngBlob.arrayBuffer();
|
const pngBytes = await pngBlob.arrayBuffer();
|
||||||
|
|
||||||
const pngImage = await pdfDoc.embedPng(pngBytes);
|
const pngImage = await pdfDoc.embedPng(pngBytes);
|
||||||
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
const page = pdfDoc.addPage([pngImage.width, pngImage.height]);
|
||||||
page.drawImage(pngImage, { x: 0, y: 0, width: pngImage.width, height: pngImage.height });
|
page.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: pngImage.width,
|
||||||
|
height: pngImage.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_heic.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_heic.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert HEIC to PDF. One of the files may be invalid or unsupported.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,12 @@ function sanitizeImageAsJpeg(imageBytes: any) {
|
|||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
canvas.toBlob(
|
canvas.toBlob(
|
||||||
async (jpegBlob) => {
|
async (jpegBlob) => {
|
||||||
if (!jpegBlob) return reject(new Error('Canvas to JPEG conversion failed.'));
|
if (!jpegBlob)
|
||||||
|
return reject(new Error('Canvas to JPEG conversion failed.'));
|
||||||
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
|
resolve(new Uint8Array(await jpegBlob.arrayBuffer()));
|
||||||
}, 'image/jpeg', 0.9
|
},
|
||||||
|
'image/jpeg',
|
||||||
|
0.9
|
||||||
);
|
);
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
};
|
};
|
||||||
@@ -56,12 +59,11 @@ function sanitizeImageAsPng(imageBytes: any) {
|
|||||||
canvas.height = img.naturalHeight;
|
canvas.height = img.naturalHeight;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
canvas.toBlob(
|
canvas.toBlob(async (pngBlob) => {
|
||||||
async (pngBlob) => {
|
if (!pngBlob)
|
||||||
if (!pngBlob) return reject(new Error('Canvas to PNG conversion failed.'));
|
return reject(new Error('Canvas to PNG conversion failed.'));
|
||||||
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
|
resolve(new Uint8Array(await pngBlob.arrayBuffer()));
|
||||||
}, 'image/png'
|
}, 'image/png');
|
||||||
);
|
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
@@ -72,7 +74,6 @@ function sanitizeImageAsPng(imageBytes: any) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function imageToPdf() {
|
export async function imageToPdf() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select at least one image file.');
|
showAlert('No Files', 'Please select at least one image file.');
|
||||||
@@ -84,7 +85,7 @@ export async function imageToPdf() {
|
|||||||
const imageList = document.getElementById('image-list');
|
const imageList = document.getElementById('image-list');
|
||||||
const sortedFiles = Array.from(imageList.children)
|
const sortedFiles = Array.from(imageList.children)
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
.map(li => state.files.find(f => f.name === li.dataset.fileName))
|
.map((li) => state.files.find((f) => f.name === li.dataset.fileName))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
for (const file of sortedFiles) {
|
for (const file of sortedFiles) {
|
||||||
@@ -95,7 +96,9 @@ export async function imageToPdf() {
|
|||||||
try {
|
try {
|
||||||
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
|
image = await pdfDoc.embedJpg(fileBuffer as Uint8Array);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`);
|
console.warn(
|
||||||
|
`Direct JPG embedding failed for ${file.name}, sanitizing to JPG...`
|
||||||
|
);
|
||||||
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
|
const sanitizedBytes = await sanitizeImageAsJpeg(fileBuffer);
|
||||||
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
image = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||||
}
|
}
|
||||||
@@ -103,27 +106,41 @@ export async function imageToPdf() {
|
|||||||
try {
|
try {
|
||||||
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
|
image = await pdfDoc.embedPng(fileBuffer as Uint8Array);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`);
|
console.warn(
|
||||||
|
`Direct PNG embedding failed for ${file.name}, sanitizing to PNG...`
|
||||||
|
);
|
||||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For WebP and other types, convert to PNG to preserve transparency
|
// For WebP and other types, convert to PNG to preserve transparency
|
||||||
console.warn(`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`);
|
console.warn(
|
||||||
|
`Unsupported type "${file.type}" for ${file.name}, converting to PNG...`
|
||||||
|
);
|
||||||
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
const sanitizedBytes = await sanitizeImageAsPng(fileBuffer);
|
||||||
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
image = await pdfDoc.embedPng(sanitizedBytes as Uint8Array);
|
||||||
}
|
}
|
||||||
|
|
||||||
const page = pdfDoc.addPage([image.width, image.height]);
|
const page = pdfDoc.addPage([image.width, image.height]);
|
||||||
page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
page.drawImage(image, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pdfDoc.getPageCount() === 0) {
|
if (pdfDoc.getPageCount() === 0) {
|
||||||
throw new Error("No valid images could be processed. Please check your files.");
|
throw new Error(
|
||||||
|
'No valid images could be processed. Please check your files.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from-images.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from-images.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
showAlert('Error', e.message || 'Failed to create PDF from images.');
|
||||||
|
|||||||
+36
-6
@@ -30,7 +30,7 @@ import { txtToPdf } from './txt-to-pdf.js';
|
|||||||
import { invertColors } from './invert-colors.js';
|
import { invertColors } from './invert-colors.js';
|
||||||
// import { viewMetadata } from './view-metadata.js';
|
// import { viewMetadata } from './view-metadata.js';
|
||||||
import { reversePages } from './reverse-pages.js';
|
import { reversePages } from './reverse-pages.js';
|
||||||
import { mdToPdf } from './md-to-pdf.js';
|
// import { mdToPdf } from './md-to-pdf.js';
|
||||||
import { svgToPdf } from './svg-to-pdf.js';
|
import { svgToPdf } from './svg-to-pdf.js';
|
||||||
import { bmpToPdf } from './bmp-to-pdf.js';
|
import { bmpToPdf } from './bmp-to-pdf.js';
|
||||||
import { heicToPdf } from './heic-to-pdf.js';
|
import { heicToPdf } from './heic-to-pdf.js';
|
||||||
@@ -49,16 +49,29 @@ import { setupCompareTool } from './compare-pdfs.js';
|
|||||||
import { setupOcrTool } from './ocr-pdf.js';
|
import { setupOcrTool } from './ocr-pdf.js';
|
||||||
import { wordToPdf } from './word-to-pdf.js';
|
import { wordToPdf } from './word-to-pdf.js';
|
||||||
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
|
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
|
||||||
import { removeAnnotations, setupRemoveAnnotationsTool } from './remove-annotations.js';
|
import {
|
||||||
|
removeAnnotations,
|
||||||
|
setupRemoveAnnotationsTool,
|
||||||
|
} from './remove-annotations.js';
|
||||||
import { setupCropperTool } from './cropper.js';
|
import { setupCropperTool } from './cropper.js';
|
||||||
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
|
import { processAndDownloadForm, setupFormFiller } from './form-filler.js';
|
||||||
import { posterize, setupPosterizeTool } from './posterize.js';
|
import { posterize, setupPosterizeTool } from './posterize.js';
|
||||||
|
import {
|
||||||
|
removeBlankPages,
|
||||||
|
setupRemoveBlankPagesTool,
|
||||||
|
} from './remove-blank-pages.js';
|
||||||
|
import { alternateMerge, setupAlternateMergeTool } from './alternate-merge.js';
|
||||||
|
import { linearizePdf } from './linearize.js';
|
||||||
|
import { addAttachments, setupAddAttachmentsTool } from './add-attachments.js';
|
||||||
|
import { sanitizePdf } from './sanitize-pdf.js';
|
||||||
|
import { removeRestrictions } from './remove-restrictions.js';
|
||||||
|
|
||||||
export const toolLogic = {
|
export const toolLogic = {
|
||||||
merge: { process: merge, setup: setupMergeTool },
|
merge: { process: merge, setup: setupMergeTool },
|
||||||
split: { process: split, setup: setupSplitTool },
|
split: { process: split, setup: setupSplitTool },
|
||||||
encrypt,
|
encrypt,
|
||||||
decrypt,
|
decrypt,
|
||||||
|
'remove-restrictions': removeRestrictions,
|
||||||
organize,
|
organize,
|
||||||
rotate,
|
rotate,
|
||||||
'add-page-numbers': addPageNumbers,
|
'add-page-numbers': addPageNumbers,
|
||||||
@@ -86,7 +99,7 @@ export const toolLogic = {
|
|||||||
'txt-to-pdf': txtToPdf,
|
'txt-to-pdf': txtToPdf,
|
||||||
'invert-colors': invertColors,
|
'invert-colors': invertColors,
|
||||||
'reverse-pages': reversePages,
|
'reverse-pages': reversePages,
|
||||||
'md-to-pdf': mdToPdf,
|
// 'md-to-pdf': mdToPdf,
|
||||||
'svg-to-pdf': svgToPdf,
|
'svg-to-pdf': svgToPdf,
|
||||||
'bmp-to-pdf': bmpToPdf,
|
'bmp-to-pdf': bmpToPdf,
|
||||||
'heic-to-pdf': heicToPdf,
|
'heic-to-pdf': heicToPdf,
|
||||||
@@ -105,8 +118,25 @@ export const toolLogic = {
|
|||||||
'ocr-pdf': { setup: setupOcrTool },
|
'ocr-pdf': { setup: setupOcrTool },
|
||||||
'word-to-pdf': wordToPdf,
|
'word-to-pdf': wordToPdf,
|
||||||
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
|
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
|
||||||
'remove-annotations': { process: removeAnnotations, setup: setupRemoveAnnotationsTool },
|
'remove-annotations': {
|
||||||
'cropper': { setup: setupCropperTool },
|
process: removeAnnotations,
|
||||||
|
setup: setupRemoveAnnotationsTool,
|
||||||
|
},
|
||||||
|
cropper: { setup: setupCropperTool },
|
||||||
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller },
|
'form-filler': { process: processAndDownloadForm, setup: setupFormFiller },
|
||||||
'posterize': { process: posterize, setup: setupPosterizeTool },
|
posterize: { process: posterize, setup: setupPosterizeTool },
|
||||||
|
'remove-blank-pages': {
|
||||||
|
process: removeBlankPages,
|
||||||
|
setup: setupRemoveBlankPagesTool,
|
||||||
|
},
|
||||||
|
'alternate-merge': {
|
||||||
|
process: alternateMerge,
|
||||||
|
setup: setupAlternateMergeTool,
|
||||||
|
},
|
||||||
|
linearize: linearizePdf,
|
||||||
|
'add-attachments': {
|
||||||
|
process: addAttachments,
|
||||||
|
setup: setupAddAttachmentsTool,
|
||||||
|
},
|
||||||
|
'sanitize-pdf': sanitizePdf,
|
||||||
};
|
};
|
||||||
@@ -4,7 +4,10 @@ import { state } from '../state.js';
|
|||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
|
||||||
export async function invertColors() {
|
export async function invertColors() {
|
||||||
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
|
if (!state.pdfDoc) {
|
||||||
|
showAlert('Error', 'PDF not loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
showLoader('Inverting PDF colors...');
|
showLoader('Inverting PDF colors...');
|
||||||
try {
|
try {
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
@@ -30,19 +33,29 @@ export async function invertColors() {
|
|||||||
}
|
}
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
const pngImageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}, 'image/png'));
|
}, 'image/png')
|
||||||
|
);
|
||||||
|
|
||||||
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
const image = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||||
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
newPage.drawImage(image, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'inverted.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'inverted.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not invert PDF colors.');
|
showAlert('Error', 'Could not invert PDF colors.');
|
||||||
|
|||||||
@@ -39,7 +39,11 @@ function sanitizeImageAsJpeg(imageBytes: any) {
|
|||||||
|
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
URL.revokeObjectURL(imageUrl);
|
URL.revokeObjectURL(imageUrl);
|
||||||
reject(new Error('The provided file could not be loaded as an image. It may be corrupted.'));
|
reject(
|
||||||
|
new Error(
|
||||||
|
'The provided file could not be loaded as an image. It may be corrupted.'
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
img.src = imageUrl;
|
img.src = imageUrl;
|
||||||
@@ -63,13 +67,20 @@ export async function jpgToPdf() {
|
|||||||
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
jpgImage = await pdfDoc.embedJpg(originalBytes as Uint8Array);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
|
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 1.
|
||||||
showAlert(`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`);
|
showAlert(
|
||||||
|
`Direct JPG embedding failed for ${file.name}, attempting to sanitize...`
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
const sanitizedBytes = await sanitizeImageAsJpeg(originalBytes);
|
||||||
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
jpgImage = await pdfDoc.embedJpg(sanitizedBytes as Uint8Array);
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error(`Failed to process ${file.name} after sanitization:`, fallbackError);
|
console.error(
|
||||||
throw new Error(`Could not process "${file.name}". The file may be corrupted.`);
|
`Failed to process ${file.name} after sanitization:`,
|
||||||
|
fallbackError
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`Could not process "${file.name}". The file may be corrupted.`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +94,10 @@ export async function jpgToPdf() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_jpgs.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_jpgs.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Conversion Error', e.message);
|
showAlert('Conversion Error', e.message);
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import JSZip from 'jszip'
|
||||||
|
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers';
|
||||||
|
|
||||||
|
const worker = new Worker(new URL('/workers/json-to-pdf.worker.js', import.meta.url));
|
||||||
|
|
||||||
|
let selectedFiles: File[] = []
|
||||||
|
|
||||||
|
const jsonFilesInput = document.getElementById('jsonFiles') as HTMLInputElement
|
||||||
|
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement
|
||||||
|
const statusMessage = document.getElementById('status-message') as HTMLDivElement
|
||||||
|
const fileListDiv = document.getElementById('fileList') as HTMLDivElement
|
||||||
|
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement
|
||||||
|
|
||||||
|
function showStatus(
|
||||||
|
message: string,
|
||||||
|
type: 'success' | 'error' | 'info' = 'info'
|
||||||
|
) {
|
||||||
|
statusMessage.textContent = message
|
||||||
|
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
|
||||||
|
type === 'success'
|
||||||
|
? 'bg-green-900 text-green-200'
|
||||||
|
: type === 'error'
|
||||||
|
? 'bg-red-900 text-red-200'
|
||||||
|
: 'bg-blue-900 text-blue-200'
|
||||||
|
}`
|
||||||
|
statusMessage.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideStatus() {
|
||||||
|
statusMessage.classList.add('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileList() {
|
||||||
|
fileListDiv.innerHTML = ''
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
fileListDiv.classList.add('hidden')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileListDiv.classList.remove('hidden')
|
||||||
|
selectedFiles.forEach((file) => {
|
||||||
|
const fileDiv = document.createElement('div')
|
||||||
|
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2'
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('span')
|
||||||
|
nameSpan.className = 'truncate font-medium text-gray-200'
|
||||||
|
nameSpan.textContent = file.name
|
||||||
|
|
||||||
|
const sizeSpan = document.createElement('span')
|
||||||
|
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'
|
||||||
|
sizeSpan.textContent = formatBytes(file.size)
|
||||||
|
|
||||||
|
fileDiv.append(nameSpan, sizeSpan)
|
||||||
|
fileListDiv.appendChild(fileDiv)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonFilesInput.addEventListener('change', (e) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
if (target.files && target.files.length > 0) {
|
||||||
|
selectedFiles = Array.from(target.files)
|
||||||
|
convertBtn.disabled = selectedFiles.length === 0
|
||||||
|
updateFileList()
|
||||||
|
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
showStatus('Please select at least 1 JSON file', 'info')
|
||||||
|
} else {
|
||||||
|
showStatus(`${selectedFiles.length} file(s) selected. Ready to convert!`, 'info')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function convertJSONsToPDF() {
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
showStatus('Please select at least 1 JSON file', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
convertBtn.disabled = true
|
||||||
|
showStatus('Reading files (Main Thread)...', 'info')
|
||||||
|
|
||||||
|
const fileBuffers = await Promise.all(
|
||||||
|
selectedFiles.map(file => readFileAsArrayBuffer(file))
|
||||||
|
)
|
||||||
|
|
||||||
|
showStatus('Converting JSONs to PDFs...', 'info')
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
command: 'convert',
|
||||||
|
fileBuffers: fileBuffers,
|
||||||
|
fileNames: selectedFiles.map(f => f.name)
|
||||||
|
}, fileBuffers);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading files:', error)
|
||||||
|
showStatus(`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
|
||||||
|
convertBtn.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onmessage = async (e: MessageEvent) => {
|
||||||
|
convertBtn.disabled = false;
|
||||||
|
|
||||||
|
if (e.data.status === 'success') {
|
||||||
|
const pdfFiles = e.data.pdfFiles as Array<{ name: string, data: ArrayBuffer }>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
showStatus('Creating ZIP file...', 'info')
|
||||||
|
|
||||||
|
const zip = new JSZip()
|
||||||
|
pdfFiles.forEach(({ name, data }) => {
|
||||||
|
const pdfName = name.replace(/\.json$/i, '.pdf')
|
||||||
|
const uint8Array = new Uint8Array(data)
|
||||||
|
zip.file(pdfName, uint8Array)
|
||||||
|
})
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' })
|
||||||
|
const url = URL.createObjectURL(zipBlob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = 'jsons-to-pdf.zip'
|
||||||
|
downloadFile(zipBlob, 'jsons-to-pdf.zip')
|
||||||
|
|
||||||
|
showStatus('✅ JSONs converted to PDF successfully! ZIP download started.', 'success')
|
||||||
|
|
||||||
|
selectedFiles = []
|
||||||
|
jsonFilesInput.value = ''
|
||||||
|
fileListDiv.innerHTML = ''
|
||||||
|
fileListDiv.classList.add('hidden')
|
||||||
|
convertBtn.disabled = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
hideStatus()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating ZIP:', error)
|
||||||
|
showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (e.data.status === 'error') {
|
||||||
|
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
||||||
|
console.error('Worker Error:', errorMessage);
|
||||||
|
showStatus(`❌ Worker Error: ${errorMessage}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (backToToolsBtn) {
|
||||||
|
backToToolsBtn.addEventListener('click', () => {
|
||||||
|
window.location.href = '../../index.html#tools-header'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
convertBtn.addEventListener('click', convertJSONsToPDF)
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
showStatus('Select JSON files to get started', 'info')
|
||||||
|
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import createModule from '@neslinesli93/qpdf-wasm';
|
||||||
|
import { showLoader, hideLoader, showAlert } from '../ui';
|
||||||
|
import { readFileAsArrayBuffer, downloadFile } from '../utils/helpers';
|
||||||
|
import { state } from '../state';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
let qpdfInstance: any = null;
|
||||||
|
|
||||||
|
async function initializeQpdf() {
|
||||||
|
if (qpdfInstance) {
|
||||||
|
return qpdfInstance;
|
||||||
|
}
|
||||||
|
showLoader('Initializing optimization engine...');
|
||||||
|
try {
|
||||||
|
qpdfInstance = await createModule({
|
||||||
|
locateFile: () => '/qpdf.wasm',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize qpdf-wasm:', error);
|
||||||
|
showAlert(
|
||||||
|
'Initialization Error',
|
||||||
|
'Could not load the optimization engine. Please refresh the page and try again.'
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
return qpdfInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function linearizePdf() {
|
||||||
|
// Check if there are files and at least one PDF
|
||||||
|
const pdfFiles = state.files.filter(
|
||||||
|
(file: File) => file.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
if (!pdfFiles || pdfFiles.length === 0) {
|
||||||
|
showAlert('No PDF Files', 'Please upload at least one PDF file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Optimizing PDFs for web view (linearizing)...');
|
||||||
|
const zip = new JSZip(); // Create a JSZip instance
|
||||||
|
let qpdf: any;
|
||||||
|
let successCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
qpdf = await initializeQpdf();
|
||||||
|
|
||||||
|
for (let i = 0; i < pdfFiles.length; i++) {
|
||||||
|
const file = pdfFiles[i];
|
||||||
|
const inputPath = `/input_${i}.pdf`;
|
||||||
|
const outputPath = `/output_${i}.pdf`;
|
||||||
|
|
||||||
|
showLoader(`Optimizing ${file.name} (${i + 1}/${pdfFiles.length})...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
|
|
||||||
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
|
|
||||||
|
const args = [inputPath, '--linearize', outputPath];
|
||||||
|
|
||||||
|
qpdf.callMain(args);
|
||||||
|
|
||||||
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
if (!outputFile || outputFile.length === 0) {
|
||||||
|
console.error(
|
||||||
|
`Linearization resulted in an empty file for ${file.name}.`
|
||||||
|
);
|
||||||
|
throw new Error(`Processing failed for ${file.name}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.file(`linearized-${file.name}`, outputFile, { binary: true });
|
||||||
|
successCount++;
|
||||||
|
} catch (fileError: any) {
|
||||||
|
errorCount++;
|
||||||
|
console.error(`Failed to linearize ${file.name}:`, fileError);
|
||||||
|
// Optionally add an error marker/file to the zip? For now, we just skip.
|
||||||
|
} finally {
|
||||||
|
// Clean up WASM filesystem for this file
|
||||||
|
try {
|
||||||
|
if (qpdf?.FS) {
|
||||||
|
if (qpdf.FS.analyzePath(inputPath).exists) {
|
||||||
|
qpdf.FS.unlink(inputPath);
|
||||||
|
}
|
||||||
|
if (qpdf.FS.analyzePath(outputPath).exists) {
|
||||||
|
qpdf.FS.unlink(outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to cleanup WASM FS for ${file.name}:`,
|
||||||
|
cleanupError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (successCount === 0) {
|
||||||
|
throw new Error('No PDF files could be linearized.');
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Generating ZIP file...');
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'linearized-pdfs.zip');
|
||||||
|
|
||||||
|
let alertMessage = `${successCount} PDF(s) linearized successfully.`;
|
||||||
|
if (errorCount > 0) {
|
||||||
|
alertMessage += ` ${errorCount} file(s) failed.`;
|
||||||
|
}
|
||||||
|
showAlert('Processing Complete', alertMessage);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Linearization process error:', error);
|
||||||
|
showAlert(
|
||||||
|
'Linearization Failed',
|
||||||
|
`An error occurred: ${error.message || 'Unknown error'}.`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
-77
@@ -1,85 +1,95 @@
|
|||||||
// note: this is a work in progress
|
// // note: this is a work in progress
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
// import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
// import { downloadFile } from '../utils/helpers.js';
|
||||||
import html2canvas from 'html2canvas';
|
// import html2canvas from 'html2canvas';
|
||||||
|
|
||||||
|
// export async function mdToPdf() {
|
||||||
|
// // @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||||
|
// if (
|
||||||
|
// typeof window.jspdf === 'undefined' ||
|
||||||
|
// typeof window.html2canvas === 'undefined'
|
||||||
|
// ) {
|
||||||
|
// showAlert(
|
||||||
|
// 'Libraries Not Ready',
|
||||||
|
// 'PDF generation libraries are loading. Please try again.'
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
|
// const markdownContent = document.getElementById('md-input').value.trim();
|
||||||
|
// if (!markdownContent) {
|
||||||
|
// showAlert('Input Required', 'Please enter some Markdown text.');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// showLoader('Generating High-Quality PDF...');
|
||||||
|
|
||||||
export async function mdToPdf() {
|
// try {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
// // @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
|
||||||
if (typeof window.jspdf === 'undefined' || typeof window.html2canvas === 'undefined') {
|
// const htmlContent = marked.parse(markdownContent);
|
||||||
showAlert('Libraries Not Ready', 'PDF generation libraries are loading. Please try again.');
|
// // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
return;
|
// const pageFormat = document.getElementById('page-format').value;
|
||||||
}
|
// // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// const orientation = document.getElementById('orientation').value;
|
||||||
const markdownContent = document.getElementById('md-input').value.trim();
|
// // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
if (!markdownContent) {
|
// const marginSize = document.getElementById('margin-size').value;
|
||||||
showAlert('Input Required', 'Please enter some Markdown text.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showLoader('Generating High-Quality PDF...');
|
|
||||||
|
|
||||||
try {
|
// const tempContainer = document.createElement('div');
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
|
// tempContainer.style.cssText =
|
||||||
const htmlContent = marked.parse(markdownContent);
|
// 'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// const styleSheet = document.createElement('style');
|
||||||
const pageFormat = document.getElementById('page-format').value;
|
// styleSheet.textContent = `
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// body { font-family: Helvetica, Arial, sans-serif; line-height: 1.6; font-size: 12px; }
|
||||||
const orientation = document.getElementById('orientation').value;
|
// h1, h2, h3 { margin: 20px 0 10px 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// h1 { font-size: 2em; } h2 { font-size: 1.5em; }
|
||||||
const marginSize = document.getElementById('margin-size').value;
|
// p, blockquote, ul, ol, pre, table { margin: 0 0 16px 0; }
|
||||||
|
// blockquote { padding: 0 1em; color: #6a737d; border-left: .25em solid #dfe2e5; }
|
||||||
|
// pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 6px; }
|
||||||
|
// code { font-family: 'Courier New', monospace; background-color: rgba(27,31,35,.05); border-radius: 3px; padding: .2em .4em; }
|
||||||
|
// table { width: 100%; border-collapse: collapse; } th, td { padding: 6px 13px; border: 1px solid #dfe2e5; }
|
||||||
|
// img { max-width: 100%; }
|
||||||
|
// `;
|
||||||
|
// tempContainer.appendChild(styleSheet);
|
||||||
|
// tempContainer.innerHTML += htmlContent;
|
||||||
|
// document.body.appendChild(tempContainer);
|
||||||
|
|
||||||
const tempContainer = document.createElement('div');
|
// const canvas = await html2canvas(tempContainer, {
|
||||||
tempContainer.style.cssText = 'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
|
// scale: 2,
|
||||||
const styleSheet = document.createElement('style');
|
// useCORS: true,
|
||||||
styleSheet.textContent = `
|
// });
|
||||||
body { font-family: Helvetica, Arial, sans-serif; line-height: 1.6; font-size: 12px; }
|
// document.body.removeChild(tempContainer);
|
||||||
h1, h2, h3 { margin: 20px 0 10px 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
|
|
||||||
h1 { font-size: 2em; } h2 { font-size: 1.5em; }
|
|
||||||
p, blockquote, ul, ol, pre, table { margin: 0 0 16px 0; }
|
|
||||||
blockquote { padding: 0 1em; color: #6a737d; border-left: .25em solid #dfe2e5; }
|
|
||||||
pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 6px; }
|
|
||||||
code { font-family: 'Courier New', monospace; background-color: rgba(27,31,35,.05); border-radius: 3px; padding: .2em .4em; }
|
|
||||||
table { width: 100%; border-collapse: collapse; } th, td { padding: 6px 13px; border: 1px solid #dfe2e5; }
|
|
||||||
img { max-width: 100%; }
|
|
||||||
`;
|
|
||||||
tempContainer.appendChild(styleSheet);
|
|
||||||
tempContainer.innerHTML += htmlContent;
|
|
||||||
document.body.appendChild(tempContainer);
|
|
||||||
|
|
||||||
const canvas = await html2canvas(tempContainer, { scale: 2, useCORS: true });
|
// // @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
||||||
document.body.removeChild(tempContainer);
|
// const { jsPDF } = window.jspdf;
|
||||||
|
// const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
|
||||||
|
// const pageFormats = { a4: [210, 297], letter: [216, 279] };
|
||||||
|
// const format = pageFormats[pageFormat];
|
||||||
|
// const [pageWidth, pageHeight] =
|
||||||
|
// orientation === 'landscape' ? [format[1], format[0]] : format;
|
||||||
|
// const margins = { narrow: 10, normal: 20, wide: 30 };
|
||||||
|
// const margin = margins[marginSize];
|
||||||
|
// const contentWidth = pageWidth - margin * 2;
|
||||||
|
// const contentHeight = pageHeight - margin * 2;
|
||||||
|
// const imgData = canvas.toDataURL('image/png');
|
||||||
|
// const imgHeight = (canvas.height * contentWidth) / canvas.width;
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
|
// let heightLeft = imgHeight;
|
||||||
const { jsPDF } = window.jspdf;
|
// let position = margin;
|
||||||
const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
|
// pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||||
const pageFormats = { 'a4': [210, 297], 'letter': [216, 279] };
|
// heightLeft -= contentHeight;
|
||||||
const format = pageFormats[pageFormat];
|
|
||||||
const [pageWidth, pageHeight] = orientation === 'landscape' ? [format[1], format[0]] : format;
|
|
||||||
const margins = { 'narrow': 10, 'normal': 20, 'wide': 30 };
|
|
||||||
const margin = margins[marginSize];
|
|
||||||
const contentWidth = pageWidth - (margin * 2);
|
|
||||||
const contentHeight = pageHeight - (margin * 2);
|
|
||||||
const imgData = canvas.toDataURL('image/png');
|
|
||||||
const imgHeight = (canvas.height * contentWidth) / canvas.width;
|
|
||||||
|
|
||||||
let heightLeft = imgHeight;
|
// while (heightLeft > 0) {
|
||||||
let position = margin;
|
// position = position - pageHeight;
|
||||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
// pdf.addPage();
|
||||||
heightLeft -= contentHeight;
|
// pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
||||||
|
// heightLeft -= contentHeight;
|
||||||
|
// }
|
||||||
|
|
||||||
while (heightLeft > 0) {
|
// const pdfBlob = pdf.output('blob');
|
||||||
position = position - pageHeight;
|
// downloadFile(pdfBlob, 'markdown-document.pdf');
|
||||||
pdf.addPage();
|
// } catch (error) {
|
||||||
pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
|
// console.error('MD to PDF conversion error:', error);
|
||||||
heightLeft -= contentHeight;
|
// showAlert('Conversion Error', 'Failed to generate PDF.');
|
||||||
}
|
// } finally {
|
||||||
|
// hideLoader();
|
||||||
const pdfBlob = pdf.output('blob');
|
// }
|
||||||
downloadFile(pdfBlob, 'markdown-document.pdf');
|
// }
|
||||||
} catch (error) {
|
|
||||||
console.error('MD to PDF conversion error:', error);
|
|
||||||
showAlert('Conversion Error', 'Failed to generate PDF.');
|
|
||||||
} finally {
|
|
||||||
hideLoader();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+55
-26
@@ -17,7 +17,7 @@ const mergeState = {
|
|||||||
sortableInstances: {},
|
sortableInstances: {},
|
||||||
isRendering: false,
|
isRendering: false,
|
||||||
cachedThumbnails: null,
|
cachedThumbnails: null,
|
||||||
lastFileHash: null
|
lastFileHash: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function parsePageRanges(rangeString: any, totalPages: any) {
|
function parsePageRanges(rangeString: any, totalPages: any) {
|
||||||
@@ -29,7 +29,14 @@ function parsePageRanges(rangeString: any, totalPages: any) {
|
|||||||
const trimmedRange = range.trim();
|
const trimmedRange = range.trim();
|
||||||
if (trimmedRange.includes('-')) {
|
if (trimmedRange.includes('-')) {
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
for (let i = start; i <= end; i++) {
|
for (let i = start; i <= end; i++) {
|
||||||
indices.add(i - 1);
|
indices.add(i - 1);
|
||||||
}
|
}
|
||||||
@@ -65,7 +72,7 @@ function initializeFileListSortable() {
|
|||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: any) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +97,14 @@ function initializePageThumbnailsSortable() {
|
|||||||
},
|
},
|
||||||
onEnd: function (evt: any) {
|
onEnd: function (evt: any) {
|
||||||
evt.item.style.opacity = '1';
|
evt.item.style.opacity = '1';
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateFileHash() {
|
function generateFileHash() {
|
||||||
return (state.files as File[]).map(f => `${f.name}-${f.size}-${f.lastModified}`).join('|');
|
return (state.files as File[])
|
||||||
|
.map((f) => `${f.name}-${f.size}-${f.lastModified}`)
|
||||||
|
.join('|');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderPageMergeThumbnails() {
|
async function renderPageMergeThumbnails() {
|
||||||
@@ -138,7 +147,9 @@ async function renderPageMergeThumbnails() {
|
|||||||
|
|
||||||
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
for (let i = 1; i <= pdfjsDoc.numPages; i++) {
|
||||||
currentPageNumber++;
|
currentPageNumber++;
|
||||||
showLoader(`Rendering page previews: ${currentPageNumber}/${totalPages}`);
|
showLoader(
|
||||||
|
`Rendering page previews: ${currentPageNumber}/${totalPages}`
|
||||||
|
);
|
||||||
const page = await pdfjsDoc.getPage(i);
|
const page = await pdfjsDoc.getPage(i);
|
||||||
const viewport = page.getViewport({ scale: 0.3 });
|
const viewport = page.getViewport({ scale: 0.3 });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@@ -148,11 +159,12 @@ async function renderPageMergeThumbnails() {
|
|||||||
await page.render({
|
await page.render({
|
||||||
canvasContext: context,
|
canvasContext: context,
|
||||||
canvas: canvas,
|
canvas: canvas,
|
||||||
viewport
|
viewport,
|
||||||
}).promise;
|
}).promise;
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
wrapper.className =
|
||||||
|
'page-thumbnail relative cursor-move flex flex-col items-center gap-1 p-2 border-2 border-gray-600 hover:border-indigo-500 rounded-lg bg-gray-700 transition-colors';
|
||||||
wrapper.dataset.fileName = file.name;
|
wrapper.dataset.fileName = file.name;
|
||||||
wrapper.dataset.pageIndex = (i - 1).toString();
|
wrapper.dataset.pageIndex = (i - 1).toString();
|
||||||
|
|
||||||
@@ -164,13 +176,15 @@ async function renderPageMergeThumbnails() {
|
|||||||
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
img.className = 'rounded-md shadow-md max-w-full h-auto';
|
||||||
|
|
||||||
const pageNumDiv = document.createElement('div');
|
const pageNumDiv = document.createElement('div');
|
||||||
pageNumDiv.className = 'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
|
pageNumDiv.className =
|
||||||
|
'absolute top-1 left-1 bg-indigo-600 text-white text-xs px-2 py-1 rounded-md font-semibold shadow-lg';
|
||||||
pageNumDiv.textContent = i.toString();
|
pageNumDiv.textContent = i.toString();
|
||||||
|
|
||||||
imgContainer.append(img, pageNumDiv);
|
imgContainer.append(img, pageNumDiv);
|
||||||
|
|
||||||
const fileNamePara = document.createElement('p');
|
const fileNamePara = document.createElement('p');
|
||||||
fileNamePara.className = 'text-xs text-gray-400 truncate w-full text-center';
|
fileNamePara.className =
|
||||||
|
'text-xs text-gray-400 truncate w-full text-center';
|
||||||
const fullTitle = `${file.name} (page ${i})`;
|
const fullTitle = `${file.name} (page ${i})`;
|
||||||
fileNamePara.title = fullTitle;
|
fileNamePara.title = fullTitle;
|
||||||
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
fileNamePara.textContent = `${file.name.substring(0, 10)}... (p${i})`;
|
||||||
@@ -202,10 +216,12 @@ export async function merge() {
|
|||||||
|
|
||||||
if (mergeState.activeMode === 'file') {
|
if (mergeState.activeMode === 'file') {
|
||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
const sortedFiles = Array.from(fileList.children).map(li => {
|
const sortedFiles = Array.from(fileList.children)
|
||||||
|
.map((li) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
return state.files.find(f => f.name === li.dataset.fileName);
|
return state.files.find((f) => f.name === li.dataset.fileName);
|
||||||
}).filter(Boolean);
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
for (const file of sortedFiles) {
|
for (const file of sortedFiles) {
|
||||||
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
const safeFileName = file.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||||
@@ -220,11 +236,11 @@ export async function merge() {
|
|||||||
const totalPages = sourcePdf.getPageCount();
|
const totalPages = sourcePdf.getPageCount();
|
||||||
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
|
const pageIndices = parsePageRanges(rangeInputValue, totalPages);
|
||||||
|
|
||||||
const indicesToCopy = pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
const indicesToCopy =
|
||||||
|
pageIndices.length > 0 ? pageIndices : sourcePdf.getPageIndices();
|
||||||
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
const copiedPages = await newPdfDoc.copyPages(sourcePdf, indicesToCopy);
|
||||||
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
copiedPages.forEach((page: any) => newPdfDoc.addPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const pageContainer = document.getElementById('page-merge-preview');
|
const pageContainer = document.getElementById('page-merge-preview');
|
||||||
const pageElements = Array.from(pageContainer.children);
|
const pageElements = Array.from(pageContainer.children);
|
||||||
@@ -237,19 +253,26 @@ export async function merge() {
|
|||||||
|
|
||||||
const sourcePdf = mergeState.pdfDocs[fileName];
|
const sourcePdf = mergeState.pdfDocs[fileName];
|
||||||
if (sourcePdf && !isNaN(pageIndex)) {
|
if (sourcePdf && !isNaN(pageIndex)) {
|
||||||
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [pageIndex]);
|
const [copiedPage] = await newPdfDoc.copyPages(sourcePdf, [
|
||||||
|
pageIndex,
|
||||||
|
]);
|
||||||
newPdfDoc.addPage(copiedPage);
|
newPdfDoc.addPage(copiedPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedPdfBytes = await newPdfDoc.save();
|
const mergedPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }), 'merged.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(mergedPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'merged.pdf'
|
||||||
|
);
|
||||||
showAlert('Success', 'PDFs merged successfully!');
|
showAlert('Success', 'PDFs merged successfully!');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Merge error:', e);
|
console.error('Merge error:', e);
|
||||||
showAlert('Error', 'Failed to merge PDFs. Please check that all files are valid and not password-protected.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to merge PDFs. Please check that all files are valid and not password-protected.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
@@ -267,9 +290,12 @@ export async function setupMergeTool() {
|
|||||||
for (const file of state.files) {
|
for (const file of state.files) {
|
||||||
if (!mergeState.pdfDocs[file.name]) {
|
if (!mergeState.pdfDocs[file.name]) {
|
||||||
const pdfBytes = await readFileAsArrayBuffer(file);
|
const pdfBytes = await readFileAsArrayBuffer(file);
|
||||||
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(pdfBytes as ArrayBuffer, {
|
mergeState.pdfDocs[file.name] = await PDFLibDocument.load(
|
||||||
ignoreEncryption: true
|
pdfBytes as ArrayBuffer,
|
||||||
});
|
{
|
||||||
|
ignoreEncryption: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -287,13 +313,14 @@ export async function setupMergeTool() {
|
|||||||
const fileList = document.getElementById('file-list');
|
const fileList = document.getElementById('file-list');
|
||||||
|
|
||||||
fileList.textContent = ''; // Clear list safely
|
fileList.textContent = ''; // Clear list safely
|
||||||
(state.files as File[]).forEach(f => {
|
(state.files as File[]).forEach((f) => {
|
||||||
const doc = mergeState.pdfDocs[f.name];
|
const doc = mergeState.pdfDocs[f.name];
|
||||||
const pageCount = doc ? doc.getPageCount() : 'N/A';
|
const pageCount = doc ? doc.getPageCount() : 'N/A';
|
||||||
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
const safeFileName = f.name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||||
|
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
li.className =
|
||||||
|
'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors';
|
||||||
li.dataset.fileName = f.name;
|
li.dataset.fileName = f.name;
|
||||||
|
|
||||||
const mainDiv = document.createElement('div');
|
const mainDiv = document.createElement('div');
|
||||||
@@ -305,7 +332,8 @@ export async function setupMergeTool() {
|
|||||||
nameSpan.textContent = f.name;
|
nameSpan.textContent = f.name;
|
||||||
|
|
||||||
const dragHandle = document.createElement('div');
|
const dragHandle = document.createElement('div');
|
||||||
dragHandle.className = 'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
|
dragHandle.className =
|
||||||
|
'drag-handle cursor-move text-gray-400 hover:text-white p-1 rounded transition-colors';
|
||||||
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
|
dragHandle.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="5" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="19" r="1"/></svg>`; // Safe: static content
|
||||||
|
|
||||||
mainDiv.append(nameSpan, dragHandle);
|
mainDiv.append(nameSpan, dragHandle);
|
||||||
@@ -321,7 +349,8 @@ export async function setupMergeTool() {
|
|||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'text';
|
input.type = 'text';
|
||||||
input.id = `range-${safeFileName}`;
|
input.id = `range-${safeFileName}`;
|
||||||
input.className = 'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
input.className =
|
||||||
|
'w-full bg-gray-800 border border-gray-600 text-white rounded-md p-2 text-sm mt-1 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors';
|
||||||
input.placeholder = 'Leave blank for all pages';
|
input.placeholder = 'Leave blank for all pages';
|
||||||
|
|
||||||
rangeDiv.append(label, input);
|
rangeDiv.append(label, input);
|
||||||
|
|||||||
+28
-11
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, hexToRgb } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -45,7 +44,10 @@ export async function nUpTool() {
|
|||||||
const firstPage = sourcePages[0];
|
const firstPage = sourcePages[0];
|
||||||
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
const isSourceLandscape = firstPage.getWidth() > firstPage.getHeight();
|
||||||
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
|
// If source is landscape and grid is wider than tall (like 2x1), output landscape.
|
||||||
orientation = (isSourceLandscape && gridDims[0] > gridDims[1]) ? 'landscape' : 'portrait';
|
orientation =
|
||||||
|
isSourceLandscape && gridDims[0] > gridDims[1]
|
||||||
|
? 'landscape'
|
||||||
|
: 'portrait';
|
||||||
}
|
}
|
||||||
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
if (orientation === 'landscape' && pageWidth < pageHeight) {
|
||||||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||||||
@@ -54,8 +56,8 @@ export async function nUpTool() {
|
|||||||
const margin = useMargins ? 36 : 0;
|
const margin = useMargins ? 36 : 0;
|
||||||
const gutter = useMargins ? 10 : 0;
|
const gutter = useMargins ? 10 : 0;
|
||||||
|
|
||||||
const usableWidth = pageWidth - (margin * 2);
|
const usableWidth = pageWidth - margin * 2;
|
||||||
const usableHeight = pageHeight - (margin * 2);
|
const usableHeight = pageHeight - margin * 2;
|
||||||
|
|
||||||
// Loop through the source pages in chunks of 'n'
|
// Loop through the source pages in chunks of 'n'
|
||||||
for (let i = 0; i < sourcePages.length; i += n) {
|
for (let i = 0; i < sourcePages.length; i += n) {
|
||||||
@@ -64,15 +66,20 @@ export async function nUpTool() {
|
|||||||
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
const outputPage = newDoc.addPage([pageWidth, pageHeight]);
|
||||||
|
|
||||||
// Calculate dimensions of each cell in the grid
|
// Calculate dimensions of each cell in the grid
|
||||||
const cellWidth = (usableWidth - (gutter * (gridDims[0] - 1))) / gridDims[0];
|
const cellWidth =
|
||||||
const cellHeight = (usableHeight - (gutter * (gridDims[1] - 1))) / gridDims[1];
|
(usableWidth - gutter * (gridDims[0] - 1)) / gridDims[0];
|
||||||
|
const cellHeight =
|
||||||
|
(usableHeight - gutter * (gridDims[1] - 1)) / gridDims[1];
|
||||||
|
|
||||||
for (let j = 0; j < chunk.length; j++) {
|
for (let j = 0; j < chunk.length; j++) {
|
||||||
const sourcePage = chunk[j];
|
const sourcePage = chunk[j];
|
||||||
const embeddedPage = await newDoc.embedPage(sourcePage);
|
const embeddedPage = await newDoc.embedPage(sourcePage);
|
||||||
|
|
||||||
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
// Calculate scaled dimensions to fit the cell, preserving aspect ratio
|
||||||
const scale = Math.min(cellWidth / embeddedPage.width, cellHeight / embeddedPage.height);
|
const scale = Math.min(
|
||||||
|
cellWidth / embeddedPage.width,
|
||||||
|
cellHeight / embeddedPage.height
|
||||||
|
);
|
||||||
const scaledWidth = embeddedPage.width * scale;
|
const scaledWidth = embeddedPage.width * scale;
|
||||||
const scaledHeight = embeddedPage.height * scale;
|
const scaledHeight = embeddedPage.height * scale;
|
||||||
|
|
||||||
@@ -80,17 +87,24 @@ export async function nUpTool() {
|
|||||||
const row = Math.floor(j / gridDims[0]);
|
const row = Math.floor(j / gridDims[0]);
|
||||||
const col = j % gridDims[0];
|
const col = j % gridDims[0];
|
||||||
const cellX = margin + col * (cellWidth + gutter);
|
const cellX = margin + col * (cellWidth + gutter);
|
||||||
const cellY = pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
const cellY =
|
||||||
|
pageHeight - margin - (row + 1) * cellHeight - row * gutter;
|
||||||
|
|
||||||
// Center the page within its cell
|
// Center the page within its cell
|
||||||
const x = cellX + (cellWidth - scaledWidth) / 2;
|
const x = cellX + (cellWidth - scaledWidth) / 2;
|
||||||
const y = cellY + (cellHeight - scaledHeight) / 2;
|
const y = cellY + (cellHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
outputPage.drawPage(embeddedPage, { x, y, width: scaledWidth, height: scaledHeight });
|
outputPage.drawPage(embeddedPage, {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: scaledWidth,
|
||||||
|
height: scaledHeight,
|
||||||
|
});
|
||||||
|
|
||||||
if (addBorder) {
|
if (addBorder) {
|
||||||
outputPage.drawRectangle({
|
outputPage.drawRectangle({
|
||||||
x, y,
|
x,
|
||||||
|
y,
|
||||||
width: scaledWidth,
|
width: scaledWidth,
|
||||||
height: scaledHeight,
|
height: scaledHeight,
|
||||||
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
borderColor: rgb(borderColor.r, borderColor.g, borderColor.b),
|
||||||
@@ -101,7 +115,10 @@ export async function nUpTool() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
const newPdfBytes = await newDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), `n-up_${n}.pdf`);
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
`n-up_${n}.pdf`
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
showAlert('Error', 'An error occurred while creating the N-Up PDF.');
|
||||||
|
|||||||
+109
-39
@@ -4,14 +4,14 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import Tesseract from 'tesseract.js';
|
import Tesseract from 'tesseract.js';
|
||||||
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument, StandardFonts, rgb } from 'pdf-lib';
|
||||||
import { icons, createIcons } from "lucide";
|
import { icons, createIcons } from 'lucide';
|
||||||
|
|
||||||
let searchablePdfBytes: any = null;
|
let searchablePdfBytes: any = null;
|
||||||
|
|
||||||
|
|
||||||
function sanitizeTextForWinAnsi(text: string): string {
|
function sanitizeTextForWinAnsi(text: string): string {
|
||||||
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
|
// Remove invisible Unicode control characters (like Left-to-Right Mark U+200E)
|
||||||
return text.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
|
return text
|
||||||
|
.replace(/[\u0000-\u001F\u007F-\u009F\u200E\u200F\u202A-\u202E\uFEFF]/g, '')
|
||||||
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
.replace(/[^\u0020-\u007E\u00A0-\u00FF]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ function parseHOCR(hocrText: string) {
|
|||||||
x0: parseInt(bboxMatch[1]),
|
x0: parseInt(bboxMatch[1]),
|
||||||
y0: parseInt(bboxMatch[2]),
|
y0: parseInt(bboxMatch[2]),
|
||||||
x1: parseInt(bboxMatch[3]),
|
x1: parseInt(bboxMatch[3]),
|
||||||
y1: parseInt(bboxMatch[4])
|
y1: parseInt(bboxMatch[4]),
|
||||||
},
|
},
|
||||||
confidence: confMatch ? parseInt(confMatch[1]) : 0
|
confidence: confMatch ? parseInt(confMatch[1]) : 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -56,7 +56,8 @@ function binarizeCanvas(ctx: any) {
|
|||||||
const data = imageData.data;
|
const data = imageData.data;
|
||||||
for (let i = 0; i < data.length; i += 4) {
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
// A simple luminance-based threshold for determining black or white
|
// A simple luminance-based threshold for determining black or white
|
||||||
const brightness = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
const brightness =
|
||||||
|
0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
||||||
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
const color = brightness > 128 ? 255 : 0; // If brighter than mid-gray, make it white, otherwise black
|
||||||
data[i] = data[i + 1] = data[i + 2] = color;
|
data[i] = data[i + 1] = data[i + 2] = color;
|
||||||
}
|
}
|
||||||
@@ -79,10 +80,10 @@ function updateProgress(status: any, progress: any) {
|
|||||||
progressLog.scrollTop = progressLog.scrollHeight;
|
progressLog.scrollTop = progressLog.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function runOCR() {
|
async function runOCR() {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
const selectedLangs = Array.from(
|
||||||
const selectedLangs = Array.from(document.querySelectorAll('.lang-checkbox:checked')).map(cb => cb.value);
|
document.querySelectorAll('.lang-checkbox:checked')
|
||||||
|
).map((cb) => (cb as HTMLInputElement).value);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const scale = parseFloat(document.getElementById('ocr-resolution').value);
|
const scale = parseFloat(document.getElementById('ocr-resolution').value);
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'HTMLEle... Remove this comment to see the full error message
|
||||||
@@ -91,7 +92,10 @@ async function runOCR() {
|
|||||||
const whitelist = document.getElementById('ocr-whitelist').value;
|
const whitelist = document.getElementById('ocr-whitelist').value;
|
||||||
|
|
||||||
if (selectedLangs.length === 0) {
|
if (selectedLangs.length === 0) {
|
||||||
showAlert('No Languages Selected', 'Please select at least one language for OCR.');
|
showAlert(
|
||||||
|
'No Languages Selected',
|
||||||
|
'Please select at least one language for OCR.'
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const langString = selectedLangs.join('+');
|
const langString = selectedLangs.join('+');
|
||||||
@@ -101,7 +105,7 @@ async function runOCR() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const worker = await Tesseract.createWorker(langString, 1, {
|
const worker = await Tesseract.createWorker(langString, 1, {
|
||||||
logger: (m: any) => updateProgress(m.status, m.progress || 0)
|
logger: (m: any) => updateProgress(m.status, m.progress || 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable hOCR output
|
// Enable hOCR output
|
||||||
@@ -109,20 +113,23 @@ async function runOCR() {
|
|||||||
tessjs_create_hocr: '1',
|
tessjs_create_hocr: '1',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (whitelist.trim()) {
|
|
||||||
await worker.setParameters({
|
await worker.setParameters({
|
||||||
tessedit_char_whitelist: whitelist.trim(),
|
tessedit_char_whitelist: whitelist,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const newPdfDoc = await PDFLibDocument.create();
|
const newPdfDoc = await PDFLibDocument.create();
|
||||||
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
const font = await newPdfDoc.embedFont(StandardFonts.Helvetica);
|
||||||
let fullText = '';
|
let fullText = '';
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
updateProgress(`Processing page ${i} of ${pdf.numPages}`, (i - 1) / pdf.numPages);
|
updateProgress(
|
||||||
|
`Processing page ${i} of ${pdf.numPages}`,
|
||||||
|
(i - 1) / pdf.numPages
|
||||||
|
);
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const viewport = page.getViewport({ scale });
|
const viewport = page.getViewport({ scale });
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@@ -135,17 +142,28 @@ async function runOCR() {
|
|||||||
binarizeCanvas(context);
|
binarizeCanvas(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await worker.recognize(canvas);
|
const result = await worker.recognize(
|
||||||
|
canvas,
|
||||||
|
{},
|
||||||
|
{ text: true, hocr: true }
|
||||||
|
);
|
||||||
const data = result.data;
|
const data = result.data;
|
||||||
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
const newPage = newPdfDoc.addPage([viewport.width, viewport.height]);
|
||||||
const pngImageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
const pngImageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}, 'image/png'));
|
}, 'image/png')
|
||||||
|
);
|
||||||
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
const pngImage = await newPdfDoc.embedPng(pngImageBytes as ArrayBuffer);
|
||||||
newPage.drawImage(pngImage, { x: 0, y: 0, width: viewport.width, height: viewport.height });
|
newPage.drawImage(pngImage, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: viewport.width,
|
||||||
|
height: viewport.height,
|
||||||
|
});
|
||||||
|
|
||||||
// Parse hOCR to get word-level data
|
// Parse hOCR to get word-level data
|
||||||
if (data.hocr) {
|
if (data.hocr) {
|
||||||
@@ -198,8 +216,13 @@ async function runOCR() {
|
|||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
document.getElementById('ocr-text-output').value = fullText.trim();
|
document.getElementById('ocr-text-output').value = fullText.trim();
|
||||||
|
|
||||||
document.getElementById('download-searchable-pdf').addEventListener('click', () => {
|
document
|
||||||
downloadFile(new Blob([searchablePdfBytes], { type: 'application/pdf' }), 'searchable.pdf');
|
.getElementById('download-searchable-pdf')
|
||||||
|
.addEventListener('click', () => {
|
||||||
|
downloadFile(
|
||||||
|
new Blob([searchablePdfBytes], { type: 'application/pdf' }),
|
||||||
|
'searchable.pdf'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// CHANGE: The copy button logic is updated to be safer.
|
// CHANGE: The copy button logic is updated to be safer.
|
||||||
@@ -230,49 +253,96 @@ async function runOCR() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('download-txt-btn').addEventListener('click', () => {
|
document
|
||||||
|
.getElementById('download-txt-btn')
|
||||||
|
.addEventListener('click', () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
||||||
const textToSave = document.getElementById('ocr-text-output').value;
|
const textToSave = document.getElementById('ocr-text-output').value;
|
||||||
const blob = new Blob([textToSave], { type: 'text/plain' });
|
const blob = new Blob([textToSave], { type: 'text/plain' });
|
||||||
downloadFile(blob, 'ocr-text.txt');
|
downloadFile(blob, 'ocr-text.txt');
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('OCR Error', 'An error occurred during the OCR process. The worker may have failed to load. Please try again.');
|
showAlert(
|
||||||
|
'OCR Error',
|
||||||
|
'An error occurred during the OCR process. The worker may have failed to load. Please try again.'
|
||||||
|
);
|
||||||
document.getElementById('ocr-options').classList.remove('hidden');
|
document.getElementById('ocr-options').classList.remove('hidden');
|
||||||
document.getElementById('ocr-progress').classList.add('hidden');
|
document.getElementById('ocr-progress').classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the UI and event listeners for the OCR tool.
|
* Sets up the UI and event listeners for the OCR tool.
|
||||||
*/
|
*/
|
||||||
export function setupOcrTool() {
|
export function setupOcrTool() {
|
||||||
const langSearch = document.getElementById('lang-search');
|
const langSearch = document.getElementById('lang-search');
|
||||||
const langList = document.getElementById('lang-list');
|
const langList = document.getElementById('lang-list');
|
||||||
const selectedLangsDisplay = document.getElementById('selected-langs-display');
|
const selectedLangsDisplay = document.getElementById(
|
||||||
|
'selected-langs-display'
|
||||||
|
);
|
||||||
const processBtn = document.getElementById('process-btn');
|
const processBtn = document.getElementById('process-btn');
|
||||||
|
|
||||||
|
// Whitelist presets
|
||||||
|
const whitelistPresets: Record<string, string> = {
|
||||||
|
alphanumeric:
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?-\'"',
|
||||||
|
'numbers-currency': '0123456789$€£¥.,- ',
|
||||||
|
'letters-only': 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz ',
|
||||||
|
'numbers-only': '0123456789',
|
||||||
|
invoice: '0123456789$.,/-#: ',
|
||||||
|
forms:
|
||||||
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,()-_/@#:',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle whitelist preset selection
|
||||||
|
const presetSelect = document.getElementById(
|
||||||
|
'whitelist-preset'
|
||||||
|
) as HTMLSelectElement;
|
||||||
|
const whitelistInput = document.getElementById(
|
||||||
|
'ocr-whitelist'
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
|
presetSelect?.addEventListener('change', (e) => {
|
||||||
|
const preset = (e.target as HTMLSelectElement).value;
|
||||||
|
if (preset && preset !== 'custom') {
|
||||||
|
whitelistInput.value = whitelistPresets[preset];
|
||||||
|
whitelistInput.disabled = true;
|
||||||
|
} else {
|
||||||
|
whitelistInput.disabled = false;
|
||||||
|
if (preset === '') {
|
||||||
|
whitelistInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle details toggle icon rotation
|
||||||
|
document.querySelectorAll('details').forEach((details) => {
|
||||||
|
details.addEventListener('toggle', () => {
|
||||||
|
const icon = details.querySelector('.details-icon') as HTMLElement;
|
||||||
|
if (icon) {
|
||||||
|
icon.style.transform = details.open ? 'rotate(180deg)' : 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
langSearch.addEventListener('input', () => {
|
langSearch.addEventListener('input', () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
|
const searchTerm = (langSearch as HTMLInputElement).value.toLowerCase();
|
||||||
const searchTerm = langSearch.value.toLowerCase();
|
langList.querySelectorAll('label').forEach((label) => {
|
||||||
langList.querySelectorAll('label').forEach(label => {
|
label.style.display = label.textContent.toLowerCase().includes(searchTerm)
|
||||||
label.style.display = label.textContent.toLowerCase().includes(searchTerm) ? '' : 'none';
|
? ''
|
||||||
|
: 'none';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the display of selected languages
|
|
||||||
langList.addEventListener('change', () => {
|
langList.addEventListener('change', () => {
|
||||||
const selected = Array.from(langList.querySelectorAll('.lang-checkbox:checked'))
|
const selected = Array.from(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
langList.querySelectorAll('.lang-checkbox:checked')
|
||||||
.map(cb => tesseractLanguages[cb.value]);
|
).map((cb) => tesseractLanguages[(cb as HTMLInputElement).value]);
|
||||||
selectedLangsDisplay.textContent = selected.length > 0 ? selected.join(', ') : 'None';
|
selectedLangsDisplay.textContent =
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'disabled' does not exist on type 'HTMLEl... Remove this comment to see the full error message
|
selected.length > 0 ? selected.join(', ') : 'None';
|
||||||
processBtn.disabled = selected.length === 0;
|
(processBtn as HTMLButtonElement).disabled = selected.length === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attach the main OCR function to the process button
|
|
||||||
processBtn.addEventListener('click', runOCR);
|
processBtn.addEventListener('click', runOCR);
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -10,14 +9,18 @@ export async function organize() {
|
|||||||
try {
|
try {
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const pageContainer = document.getElementById('page-organizer');
|
const pageContainer = document.getElementById('page-organizer');
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'dataset' does not exist on type 'Element... Remove this comment to see the full error message
|
const pageIndices = Array.from(pageContainer.children).map((child) =>
|
||||||
const pageIndices = Array.from(pageContainer.children).map(child => parseInt(child.dataset.pageIndex));
|
parseInt((child as HTMLElement).dataset.pageIndex)
|
||||||
|
);
|
||||||
|
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
const copiedPages = await newPdf.copyPages(state.pdfDoc, pageIndices);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'organized.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'organized.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not save the changes.');
|
showAlert('Error', 'Could not save the changes.');
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function renderTable(unit: any) {
|
|||||||
|
|
||||||
tableBody.textContent = ''; // Clear the table body safely
|
tableBody.textContent = ''; // Clear the table body safely
|
||||||
|
|
||||||
analyzedPagesData.forEach(pageData => {
|
analyzedPagesData.forEach((pageData) => {
|
||||||
const width = convertPoints(pageData.width, unit);
|
const width = convertPoints(pageData.width, unit);
|
||||||
const height = convertPoints(pageData.height, unit);
|
const height = convertPoints(pageData.height, unit);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function encodeBMP(imageData: any) {
|
|||||||
const view = new DataView(buffer);
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
// BMP File Header (14 bytes)
|
// BMP File Header (14 bytes)
|
||||||
view.setUint16(0, 0x4D42, true); // 'BM'
|
view.setUint16(0, 0x4d42, true); // 'BM'
|
||||||
view.setUint32(2, fileSize, true);
|
view.setUint32(2, fileSize, true);
|
||||||
view.setUint32(10, 54, true); // Offset to pixel data
|
view.setUint32(10, 54, true); // Offset to pixel data
|
||||||
|
|
||||||
@@ -43,19 +43,20 @@ function encodeBMP(imageData: any) {
|
|||||||
view.setUint8(offset++, data[i]); // Red
|
view.setUint8(offset++, data[i]); // Red
|
||||||
}
|
}
|
||||||
// Add padding to make the row a multiple of 4 bytes
|
// Add padding to make the row a multiple of 4 bytes
|
||||||
for (let p = 0; p < (stride - width * 3); p++) {
|
for (let p = 0; p < stride - width * 3; p++) {
|
||||||
view.setUint8(offset++, 0);
|
view.setUint8(offset++, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function pdfToBmp() {
|
export async function pdfToBmp() {
|
||||||
showLoader('Converting PDF to BMP images...');
|
showLoader('Converting PDF to BMP images...');
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
@@ -81,12 +82,14 @@ export async function pdfToBmp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoader('Compressing files into a ZIP...');
|
showLoader('Compressing files into a ZIP...');
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_bmp_images.zip');
|
downloadFile(zipBlob, 'converted_bmp_images.zip');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert PDF to BMP. The file might be corrupted.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert PDF to BMP. The file might be corrupted.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,19 +36,29 @@ export async function pdfToGreyscale() {
|
|||||||
}
|
}
|
||||||
ctx.putImageData(imageData, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
const imageBytes = await new Promise(resolve => canvas.toBlob(blob => {
|
const imageBytes = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
// @ts-expect-error TS(2769) FIXME: No overload matches this call.
|
||||||
reader.onload = () => resolve(new Uint8Array(reader.result));
|
reader.onload = () => resolve(new Uint8Array(reader.result));
|
||||||
reader.readAsArrayBuffer(blob);
|
reader.readAsArrayBuffer(blob);
|
||||||
}, 'image/png'));
|
}, 'image/png')
|
||||||
|
);
|
||||||
|
|
||||||
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
|
const image = await newPdfDoc.embedPng(imageBytes as Uint8Array);
|
||||||
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
const newPage = newPdfDoc.addPage([image.width, image.height]);
|
||||||
newPage.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
|
newPage.drawImage(image, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const newPdfBytes = await newPdfDoc.save();
|
const newPdfBytes = await newPdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'greyscale.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'greyscale.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not convert to greyscale.');
|
showAlert('Error', 'Could not convert to greyscale.');
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ export async function pdfToJpg() {
|
|||||||
showLoader('Converting to JPG...');
|
showLoader('Converting to JPG...');
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
@@ -20,15 +22,20 @@ export async function pdfToJpg() {
|
|||||||
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
|
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.9));
|
const blob = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob(resolve, 'image/jpeg', 0.9)
|
||||||
|
);
|
||||||
zip.file(`page_${i}.jpg`, blob as Blob);
|
zip.file(`page_${i}.jpg`, blob as Blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_images.zip');
|
downloadFile(zipBlob, 'converted_images.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert PDF to JPG. The file might be corrupted.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert PDF to JPG. The file might be corrupted.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import JSZip from 'jszip'
|
||||||
|
import { downloadFile, formatBytes, readFileAsArrayBuffer } from '../utils/helpers';
|
||||||
|
|
||||||
|
const worker = new Worker(new URL('/workers/pdf-to-json.worker.js', import.meta.url));
|
||||||
|
|
||||||
|
let selectedFiles: File[] = []
|
||||||
|
|
||||||
|
const pdfFilesInput = document.getElementById('pdfFiles') as HTMLInputElement
|
||||||
|
const convertBtn = document.getElementById('convertBtn') as HTMLButtonElement
|
||||||
|
const statusMessage = document.getElementById('status-message') as HTMLDivElement
|
||||||
|
const fileListDiv = document.getElementById('fileList') as HTMLDivElement
|
||||||
|
const backToToolsBtn = document.getElementById('back-to-tools') as HTMLButtonElement
|
||||||
|
|
||||||
|
function showStatus(
|
||||||
|
message: string,
|
||||||
|
type: 'success' | 'error' | 'info' = 'info'
|
||||||
|
) {
|
||||||
|
statusMessage.textContent = message
|
||||||
|
statusMessage.className = `mt-4 p-3 rounded-lg text-sm ${
|
||||||
|
type === 'success'
|
||||||
|
? 'bg-green-900 text-green-200'
|
||||||
|
: type === 'error'
|
||||||
|
? 'bg-red-900 text-red-200'
|
||||||
|
: 'bg-blue-900 text-blue-200'
|
||||||
|
}`
|
||||||
|
statusMessage.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideStatus() {
|
||||||
|
statusMessage.classList.add('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileList() {
|
||||||
|
fileListDiv.innerHTML = ''
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
fileListDiv.classList.add('hidden')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileListDiv.classList.remove('hidden')
|
||||||
|
selectedFiles.forEach((file) => {
|
||||||
|
const fileDiv = document.createElement('div')
|
||||||
|
fileDiv.className = 'flex items-center justify-between bg-gray-700 p-3 rounded-lg text-sm mb-2'
|
||||||
|
|
||||||
|
const nameSpan = document.createElement('span')
|
||||||
|
nameSpan.className = 'truncate font-medium text-gray-200'
|
||||||
|
nameSpan.textContent = file.name
|
||||||
|
|
||||||
|
const sizeSpan = document.createElement('span')
|
||||||
|
sizeSpan.className = 'flex-shrink-0 ml-4 text-gray-400'
|
||||||
|
sizeSpan.textContent = formatBytes(file.size)
|
||||||
|
|
||||||
|
fileDiv.append(nameSpan, sizeSpan)
|
||||||
|
fileListDiv.appendChild(fileDiv)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfFilesInput.addEventListener('change', (e) => {
|
||||||
|
const target = e.target as HTMLInputElement
|
||||||
|
if (target.files && target.files.length > 0) {
|
||||||
|
selectedFiles = Array.from(target.files)
|
||||||
|
convertBtn.disabled = selectedFiles.length === 0
|
||||||
|
updateFileList()
|
||||||
|
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
showStatus('Please select at least 1 PDF file', 'info')
|
||||||
|
} else {
|
||||||
|
showStatus(`${selectedFiles.length} file(s) selected. Ready to convert!`, 'info')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function convertPDFsToJSON() {
|
||||||
|
if (selectedFiles.length === 0) {
|
||||||
|
showStatus('Please select at least 1 PDF file', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
convertBtn.disabled = true
|
||||||
|
showStatus('Reading files (Main Thread)...', 'info')
|
||||||
|
|
||||||
|
const fileBuffers = await Promise.all(
|
||||||
|
selectedFiles.map(file => readFileAsArrayBuffer(file))
|
||||||
|
)
|
||||||
|
|
||||||
|
showStatus('Converting PDFs to JSON..', 'info')
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
command: 'convert',
|
||||||
|
fileBuffers: fileBuffers,
|
||||||
|
fileNames: selectedFiles.map(f => f.name)
|
||||||
|
}, fileBuffers);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading files:', error)
|
||||||
|
showStatus(`❌ Error reading files: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
|
||||||
|
convertBtn.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onmessage = async (e: MessageEvent) => {
|
||||||
|
convertBtn.disabled = false;
|
||||||
|
|
||||||
|
if (e.data.status === 'success') {
|
||||||
|
const jsonFiles = e.data.jsonFiles as Array<{ name: string, data: ArrayBuffer }>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
showStatus('Creating ZIP file...', 'info')
|
||||||
|
|
||||||
|
const zip = new JSZip()
|
||||||
|
jsonFiles.forEach(({ name, data }) => {
|
||||||
|
const jsonName = name.replace(/\.pdf$/i, '.json')
|
||||||
|
const uint8Array = new Uint8Array(data)
|
||||||
|
zip.file(jsonName, uint8Array)
|
||||||
|
})
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' })
|
||||||
|
downloadFile(zipBlob, 'pdfs-to-json.zip')
|
||||||
|
|
||||||
|
showStatus('✅ PDFs converted to JSON successfully! ZIP download started.', 'success')
|
||||||
|
|
||||||
|
selectedFiles = []
|
||||||
|
pdfFilesInput.value = ''
|
||||||
|
fileListDiv.innerHTML = ''
|
||||||
|
fileListDiv.classList.add('hidden')
|
||||||
|
convertBtn.disabled = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
hideStatus()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating ZIP:', error)
|
||||||
|
showStatus(`❌ Error creating ZIP: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error')
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (e.data.status === 'error') {
|
||||||
|
const errorMessage = e.data.message || 'Unknown error occurred in worker.';
|
||||||
|
console.error('Worker Error:', errorMessage);
|
||||||
|
showStatus(`❌ Worker Error: ${errorMessage}`, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (backToToolsBtn) {
|
||||||
|
backToToolsBtn.addEventListener('click', () => {
|
||||||
|
window.location.href = '../../index.html#tools-header'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
convertBtn.addEventListener('click', convertPDFsToJSON)
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
showStatus('Select PDF files to get started', 'info')
|
||||||
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -24,7 +23,10 @@ export async function pdfToMarkdown() {
|
|||||||
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
|
downloadFile(blob, file.name.replace(/\.pdf$/i, '.md'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Conversion Error', 'Failed to convert PDF. It may be image-based or corrupted.');
|
showAlert(
|
||||||
|
'Conversion Error',
|
||||||
|
'Failed to convert PDF. It may be image-based or corrupted.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
|
||||||
export async function pdfToPng() {
|
export async function pdfToPng() {
|
||||||
showLoader('Converting to PNG...');
|
showLoader('Converting to PNG...');
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
@@ -19,10 +19,12 @@ export async function pdfToPng() {
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
const blob = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob(resolve, 'image/png')
|
||||||
|
);
|
||||||
zip.file(`page_${i}.png`, blob as Blob);
|
zip.file(`page_${i}.png`, blob as Blob);
|
||||||
}
|
}
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_pngs.zip');
|
downloadFile(zipBlob, 'converted_pngs.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import UTIF from 'utif';
|
import UTIF from 'utif';
|
||||||
import * as pdfjsLib from "pdfjs-dist";
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
|
||||||
export async function pdfToTiff() {
|
export async function pdfToTiff() {
|
||||||
showLoader('Converting PDF to TIFF...');
|
showLoader('Converting PDF to TIFF...');
|
||||||
try {
|
try {
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
@@ -19,19 +21,30 @@ export async function pdfToTiff() {
|
|||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
|
|
||||||
await page.render({ canvasContext: context, viewport: viewport, canvas: canvas }).promise;
|
await page.render({
|
||||||
|
canvasContext: context,
|
||||||
|
viewport: viewport,
|
||||||
|
canvas: canvas,
|
||||||
|
}).promise;
|
||||||
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
const rgba = imageData.data;
|
const rgba = imageData.data;
|
||||||
const tiffBuffer = UTIF.encodeImage(new Uint8Array(rgba), canvas.width, canvas.height);
|
const tiffBuffer = UTIF.encodeImage(
|
||||||
|
new Uint8Array(rgba),
|
||||||
|
canvas.width,
|
||||||
|
canvas.height
|
||||||
|
);
|
||||||
|
|
||||||
zip.file(`page_${i}.tiff`, tiffBuffer);
|
zip.file(`page_${i}.tiff`, tiffBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_tiff_images.zip');
|
downloadFile(zipBlob, 'converted_tiff_images.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to convert PDF to TIFF. The file might be corrupted.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to convert PDF to TIFF. The file might be corrupted.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -8,7 +7,9 @@ export async function pdfToWebp() {
|
|||||||
showLoader('Converting to WebP...');
|
showLoader('Converting to WebP...');
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
// @ts-expect-error TS(2304) FIXME: Cannot find name 'pdfjsLib'.
|
||||||
const pdf = await pdfjsLib.getDocument(await readFileAsArrayBuffer(state.files[0])).promise;
|
const pdf = await pdfjsLib.getDocument(
|
||||||
|
await readFileAsArrayBuffer(state.files[0])
|
||||||
|
).promise;
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
@@ -18,10 +19,12 @@ export async function pdfToWebp() {
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
await page.render({ canvasContext: context, viewport: viewport }).promise;
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/webp', 0.9));
|
const blob = await new Promise((resolve) =>
|
||||||
|
canvas.toBlob(resolve, 'image/webp', 0.9)
|
||||||
|
);
|
||||||
zip.file(`page_${i}.webp`, blob as Blob);
|
zip.file(`page_${i}.webp`, blob as Blob);
|
||||||
}
|
}
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
downloadFile(zipBlob, 'converted_webp.zip');
|
downloadFile(zipBlob, 'converted_webp.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
|
||||||
export async function pdfToZip() {
|
export async function pdfToZip() {
|
||||||
if (state.files.length === 0) {
|
if (state.files.length === 0) {
|
||||||
showAlert('No Files', 'Please select one or more PDF files.');
|
showAlert('No Files', 'Please select one or more PDF files.');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
import { downloadFile, readFileAsArrayBuffer } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
@@ -25,10 +24,16 @@ export async function pngToPdf() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }), 'from_pngs.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'from_pngs.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to create PDF from PNG images. Ensure all files are valid PNGs.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
'Failed to create PDF from PNG images. Ensure all files are valid PNGs.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-35
@@ -17,7 +17,9 @@ async function renderPosterizePreview(pageNum: number) {
|
|||||||
posterizeState.currentPage = pageNum;
|
posterizeState.currentPage = pageNum;
|
||||||
showLoader(`Rendering preview for page ${pageNum}...`);
|
showLoader(`Rendering preview for page ${pageNum}...`);
|
||||||
|
|
||||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
const canvas = document.getElementById(
|
||||||
|
'posterize-preview-canvas'
|
||||||
|
) as HTMLCanvasElement;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
if (posterizeState.pageSnapshots[pageNum]) {
|
if (posterizeState.pageSnapshots[pageNum]) {
|
||||||
@@ -30,7 +32,12 @@ async function renderPosterizePreview(pageNum: number) {
|
|||||||
canvas.width = viewport.width;
|
canvas.width = viewport.width;
|
||||||
canvas.height = viewport.height;
|
canvas.height = viewport.height;
|
||||||
await page.render({ canvasContext: context, viewport }).promise;
|
await page.render({ canvasContext: context, viewport }).promise;
|
||||||
posterizeState.pageSnapshots[pageNum] = context.getImageData(0, 0, canvas.width, canvas.height);
|
posterizeState.pageSnapshots[pageNum] = context.getImageData(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
canvas.width,
|
||||||
|
canvas.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePreviewNav();
|
updatePreviewNav();
|
||||||
@@ -41,16 +48,33 @@ async function renderPosterizePreview(pageNum: number) {
|
|||||||
function drawGridOverlay() {
|
function drawGridOverlay() {
|
||||||
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
|
if (!posterizeState.pageSnapshots[posterizeState.currentPage]) return;
|
||||||
|
|
||||||
const canvas = document.getElementById('posterize-preview-canvas') as HTMLCanvasElement;
|
const canvas = document.getElementById(
|
||||||
|
'posterize-preview-canvas'
|
||||||
|
) as HTMLCanvasElement;
|
||||||
const context = canvas.getContext('2d');
|
const context = canvas.getContext('2d');
|
||||||
context.putImageData(posterizeState.pageSnapshots[posterizeState.currentPage], 0, 0);
|
context.putImageData(
|
||||||
|
posterizeState.pageSnapshots[posterizeState.currentPage],
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
const pageRangeInput = (
|
||||||
const pagesToProcess = parsePageRanges(pageRangeInput, posterizeState.pdfJsDoc.numPages);
|
document.getElementById('page-range') as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
const pagesToProcess = parsePageRanges(
|
||||||
|
pageRangeInput,
|
||||||
|
posterizeState.pdfJsDoc.numPages
|
||||||
|
);
|
||||||
|
|
||||||
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
|
if (pagesToProcess.includes(posterizeState.currentPage - 1)) {
|
||||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
const rows =
|
||||||
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
parseInt(
|
||||||
|
(document.getElementById('posterize-rows') as HTMLInputElement).value
|
||||||
|
) || 1;
|
||||||
|
const cols =
|
||||||
|
parseInt(
|
||||||
|
(document.getElementById('posterize-cols') as HTMLInputElement).value
|
||||||
|
) || 1;
|
||||||
|
|
||||||
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
context.strokeStyle = 'rgba(239, 68, 68, 0.9)';
|
||||||
context.lineWidth = 2;
|
context.lineWidth = 2;
|
||||||
@@ -78,29 +102,40 @@ function drawGridOverlay() {
|
|||||||
|
|
||||||
function updatePreviewNav() {
|
function updatePreviewNav() {
|
||||||
const currentPageSpan = document.getElementById('current-preview-page');
|
const currentPageSpan = document.getElementById('current-preview-page');
|
||||||
const prevBtn = document.getElementById('prev-preview-page') as HTMLButtonElement;
|
const prevBtn = document.getElementById(
|
||||||
const nextBtn = document.getElementById('next-preview-page') as HTMLButtonElement;
|
'prev-preview-page'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
const nextBtn = document.getElementById(
|
||||||
|
'next-preview-page'
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
|
||||||
currentPageSpan.textContent = posterizeState.currentPage.toString();
|
currentPageSpan.textContent = posterizeState.currentPage.toString();
|
||||||
prevBtn.disabled = posterizeState.currentPage <= 1;
|
prevBtn.disabled = posterizeState.currentPage <= 1;
|
||||||
nextBtn.disabled = posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
|
nextBtn.disabled =
|
||||||
|
posterizeState.currentPage >= posterizeState.pdfJsDoc.numPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupPosterizeTool() {
|
export async function setupPosterizeTool() {
|
||||||
if (state.pdfDoc) {
|
if (state.pdfDoc) {
|
||||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount().toString();
|
document.getElementById('total-pages').textContent = state.pdfDoc
|
||||||
|
.getPageCount()
|
||||||
|
.toString();
|
||||||
const pdfBytes = await state.pdfDoc.save();
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
posterizeState.pdfJsDoc = await pdfjsLib.getDocument({ data: pdfBytes })
|
||||||
|
.promise;
|
||||||
posterizeState.pageSnapshots = {};
|
posterizeState.pageSnapshots = {};
|
||||||
posterizeState.currentPage = 1;
|
posterizeState.currentPage = 1;
|
||||||
|
|
||||||
document.getElementById('total-preview-pages').textContent = posterizeState.pdfJsDoc.numPages.toString();
|
document.getElementById('total-preview-pages').textContent =
|
||||||
|
posterizeState.pdfJsDoc.numPages.toString();
|
||||||
await renderPosterizePreview(1);
|
await renderPosterizePreview(1);
|
||||||
|
|
||||||
document.getElementById('prev-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage - 1);
|
document.getElementById('prev-preview-page').onclick = () =>
|
||||||
document.getElementById('next-preview-page').onclick = () => renderPosterizePreview(posterizeState.currentPage + 1);
|
renderPosterizePreview(posterizeState.currentPage - 1);
|
||||||
|
document.getElementById('next-preview-page').onclick = () =>
|
||||||
|
renderPosterizePreview(posterizeState.currentPage + 1);
|
||||||
|
|
||||||
['posterize-rows', 'posterize-cols', 'page-range'].forEach(id => {
|
['posterize-rows', 'posterize-cols', 'page-range'].forEach((id) => {
|
||||||
document.getElementById(id).addEventListener('input', drawGridOverlay);
|
document.getElementById(id).addEventListener('input', drawGridOverlay);
|
||||||
});
|
});
|
||||||
createIcons({ icons });
|
createIcons({ icons });
|
||||||
@@ -110,14 +145,35 @@ export async function setupPosterizeTool() {
|
|||||||
export async function posterize() {
|
export async function posterize() {
|
||||||
showLoader('Posterizing PDF...');
|
showLoader('Posterizing PDF...');
|
||||||
try {
|
try {
|
||||||
const rows = parseInt((document.getElementById('posterize-rows') as HTMLInputElement).value) || 1;
|
const rows =
|
||||||
const cols = parseInt((document.getElementById('posterize-cols') as HTMLInputElement).value) || 1;
|
parseInt(
|
||||||
const pageSizeKey = (document.getElementById('output-page-size') as HTMLSelectElement).value;
|
(document.getElementById('posterize-rows') as HTMLInputElement).value
|
||||||
let orientation = (document.getElementById('output-orientation') as HTMLSelectElement).value;
|
) || 1;
|
||||||
const scalingMode = (document.querySelector('input[name="scaling-mode"]:checked') as HTMLInputElement).value;
|
const cols =
|
||||||
const overlap = parseFloat((document.getElementById('overlap') as HTMLInputElement).value) || 0;
|
parseInt(
|
||||||
const overlapUnits = (document.getElementById('overlap-units') as HTMLSelectElement).value;
|
(document.getElementById('posterize-cols') as HTMLInputElement).value
|
||||||
const pageRangeInput = (document.getElementById('page-range') as HTMLInputElement).value;
|
) || 1;
|
||||||
|
const pageSizeKey = (
|
||||||
|
document.getElementById('output-page-size') as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
let orientation = (
|
||||||
|
document.getElementById('output-orientation') as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
const scalingMode = (
|
||||||
|
document.querySelector(
|
||||||
|
'input[name="scaling-mode"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
const overlap =
|
||||||
|
parseFloat(
|
||||||
|
(document.getElementById('overlap') as HTMLInputElement).value
|
||||||
|
) || 0;
|
||||||
|
const overlapUnits = (
|
||||||
|
document.getElementById('overlap-units') as HTMLSelectElement
|
||||||
|
).value;
|
||||||
|
const pageRangeInput = (
|
||||||
|
document.getElementById('page-range') as HTMLInputElement
|
||||||
|
).value;
|
||||||
|
|
||||||
let overlapInPoints = overlap;
|
let overlapInPoints = overlap;
|
||||||
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
if (overlapUnits === 'in') overlapInPoints = overlap * 72;
|
||||||
@@ -128,7 +184,7 @@ export async function posterize() {
|
|||||||
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
const pageIndicesToProcess = parsePageRanges(pageRangeInput, totalPages);
|
||||||
|
|
||||||
if (pageIndicesToProcess.length === 0) {
|
if (pageIndicesToProcess.length === 0) {
|
||||||
throw new Error("Invalid page range specified.");
|
throw new Error('Invalid page range specified.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempCanvas = document.createElement('canvas');
|
const tempCanvas = document.createElement('canvas');
|
||||||
@@ -145,12 +201,16 @@ export async function posterize() {
|
|||||||
let currentOrientation = orientation;
|
let currentOrientation = orientation;
|
||||||
|
|
||||||
if (currentOrientation === 'auto') {
|
if (currentOrientation === 'auto') {
|
||||||
currentOrientation = viewport.width > viewport.height ? 'landscape' : 'portrait';
|
currentOrientation =
|
||||||
|
viewport.width > viewport.height ? 'landscape' : 'portrait';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
if (currentOrientation === 'landscape' && targetWidth < targetHeight) {
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||||
} else if (currentOrientation === 'portrait' && targetWidth > targetHeight) {
|
} else if (
|
||||||
|
currentOrientation === 'portrait' &&
|
||||||
|
targetWidth > targetHeight
|
||||||
|
) {
|
||||||
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
[targetWidth, targetHeight] = [targetHeight, targetWidth];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,20 +221,43 @@ export async function posterize() {
|
|||||||
for (let c = 0; c < cols; c++) {
|
for (let c = 0; c < cols; c++) {
|
||||||
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
const sx = c * tileWidth - (c > 0 ? overlapInPoints : 0);
|
||||||
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
const sy = r * tileHeight - (r > 0 ? overlapInPoints : 0);
|
||||||
const sWidth = tileWidth + (c > 0 ? overlapInPoints : 0) + (c < cols - 1 ? overlapInPoints : 0);
|
const sWidth =
|
||||||
const sHeight = tileHeight + (r > 0 ? overlapInPoints : 0) + (r < rows - 1 ? overlapInPoints : 0);
|
tileWidth +
|
||||||
|
(c > 0 ? overlapInPoints : 0) +
|
||||||
|
(c < cols - 1 ? overlapInPoints : 0);
|
||||||
|
const sHeight =
|
||||||
|
tileHeight +
|
||||||
|
(r > 0 ? overlapInPoints : 0) +
|
||||||
|
(r < rows - 1 ? overlapInPoints : 0);
|
||||||
|
|
||||||
const tileCanvas = document.createElement('canvas');
|
const tileCanvas = document.createElement('canvas');
|
||||||
tileCanvas.width = sWidth;
|
tileCanvas.width = sWidth;
|
||||||
tileCanvas.height = sHeight;
|
tileCanvas.height = sHeight;
|
||||||
tileCanvas.getContext('2d').drawImage(tempCanvas, sx, sy, sWidth, sHeight, 0, 0, sWidth, sHeight);
|
tileCanvas
|
||||||
|
.getContext('2d')
|
||||||
|
.drawImage(
|
||||||
|
tempCanvas,
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
sWidth,
|
||||||
|
sHeight,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
sWidth,
|
||||||
|
sHeight
|
||||||
|
);
|
||||||
|
|
||||||
const tileImage = await newDoc.embedPng(tileCanvas.toDataURL('image/png'));
|
const tileImage = await newDoc.embedPng(
|
||||||
|
tileCanvas.toDataURL('image/png')
|
||||||
|
);
|
||||||
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
const newPage = newDoc.addPage([targetWidth, targetHeight]);
|
||||||
|
|
||||||
const scaleX = newPage.getWidth() / sWidth;
|
const scaleX = newPage.getWidth() / sWidth;
|
||||||
const scaleY = newPage.getHeight() / sHeight;
|
const scaleY = newPage.getHeight() / sHeight;
|
||||||
const scale = scalingMode === 'fit' ? Math.min(scaleX, scaleY) : Math.max(scaleX, scaleY);
|
const scale =
|
||||||
|
scalingMode === 'fit'
|
||||||
|
? Math.min(scaleX, scaleY)
|
||||||
|
: Math.max(scaleX, scaleY);
|
||||||
|
|
||||||
const scaledWidth = sWidth * scale;
|
const scaledWidth = sWidth * scale;
|
||||||
const scaledHeight = sHeight * scale;
|
const scaledHeight = sHeight * scale;
|
||||||
@@ -190,9 +273,11 @@ export async function posterize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPdfBytes = await newDoc.save();
|
const newPdfBytes = await newDoc.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'posterized.pdf');
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'posterized.pdf'
|
||||||
|
);
|
||||||
showAlert('Success', 'Your PDF has been posterized.');
|
showAlert('Success', 'Your PDF has been posterized.');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Could not posterize the PDF.');
|
showAlert('Error', e.message || 'Could not posterize the PDF.');
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export async function redact(redactions: any, canvasScale: any) {
|
|||||||
const pdfX = r.canvasX * conversionScale;
|
const pdfX = r.canvasX * conversionScale;
|
||||||
const pdfWidth = r.canvasWidth * conversionScale;
|
const pdfWidth = r.canvasWidth * conversionScale;
|
||||||
const pdfHeight = r.canvasHeight * conversionScale;
|
const pdfHeight = r.canvasHeight * conversionScale;
|
||||||
const pdfY = pageHeight - (r.canvasY * conversionScale) - pdfHeight;
|
const pdfY = pageHeight - r.canvasY * conversionScale - pdfHeight;
|
||||||
|
|
||||||
page.drawRectangle({
|
page.drawRectangle({
|
||||||
x: pdfX,
|
x: pdfX,
|
||||||
@@ -31,7 +31,10 @@ export async function redact(redactions: any, canvasScale: any) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const redactedBytes = await state.pdfDoc.save();
|
const redactedBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([redactedBytes], { type: 'application/pdf' }), 'redacted.pdf');
|
downloadFile(
|
||||||
|
new Blob([redactedBytes], { type: 'application/pdf' }),
|
||||||
|
'redacted.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Failed to apply redactions.');
|
showAlert('Error', 'Failed to apply redactions.');
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import { PDFName } from 'pdf-lib';
|
|||||||
|
|
||||||
export function setupRemoveAnnotationsTool() {
|
export function setupRemoveAnnotationsTool() {
|
||||||
if (state.pdfDoc) {
|
if (state.pdfDoc) {
|
||||||
document.getElementById('total-pages').textContent = state.pdfDoc.getPageCount();
|
document.getElementById('total-pages').textContent =
|
||||||
|
state.pdfDoc.getPageCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
|
const pageScopeRadios = document.querySelectorAll('input[name="page-scope"]');
|
||||||
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
const pageRangeWrapper = document.getElementById('page-range-wrapper');
|
||||||
pageScopeRadios.forEach(radio => {
|
pageScopeRadios.forEach((radio) => {
|
||||||
radio.addEventListener('change', () => {
|
radio.addEventListener('change', () => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
||||||
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
|
pageRangeWrapper.classList.toggle('hidden', radio.value !== 'specific');
|
||||||
@@ -20,21 +21,66 @@ export function setupRemoveAnnotationsTool() {
|
|||||||
const selectAllCheckbox = document.getElementById('select-all-annotations');
|
const selectAllCheckbox = document.getElementById('select-all-annotations');
|
||||||
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
|
const allAnnotCheckboxes = document.querySelectorAll('.annot-checkbox');
|
||||||
selectAllCheckbox.addEventListener('change', () => {
|
selectAllCheckbox.addEventListener('change', () => {
|
||||||
allAnnotCheckboxes.forEach(checkbox => {
|
allAnnotCheckboxes.forEach((checkbox) => {
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
|
// @ts-expect-error TS(2339) FIXME: Property 'checked' does not exist on type 'Element... Remove this comment to see the full error message
|
||||||
checkbox.checked = selectAllCheckbox.checked;
|
checkbox.checked = selectAllCheckbox.checked;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeAnnotationsFromDoc(
|
||||||
|
pdfDoc,
|
||||||
|
pageIndices = null,
|
||||||
|
annotationTypes = null
|
||||||
|
) {
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
const targetPages =
|
||||||
|
pageIndices || Array.from({ length: pages.length }, (_, i) => i);
|
||||||
|
|
||||||
|
for (const pageIndex of targetPages) {
|
||||||
|
const page = pages[pageIndex];
|
||||||
|
const annotRefs = page.node.Annots()?.asArray() || [];
|
||||||
|
|
||||||
|
if (!annotationTypes) {
|
||||||
|
if (annotRefs.length > 0) {
|
||||||
|
page.node.delete(PDFName.of('Annots'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const annotsToKeep = [];
|
||||||
|
|
||||||
|
for (const ref of annotRefs) {
|
||||||
|
const annot = pdfDoc.context.lookup(ref);
|
||||||
|
const subtype = annot
|
||||||
|
.get(PDFName.of('Subtype'))
|
||||||
|
?.toString()
|
||||||
|
.substring(1);
|
||||||
|
|
||||||
|
if (!subtype || !annotationTypes.has(subtype)) {
|
||||||
|
annotsToKeep.push(ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (annotsToKeep.length > 0) {
|
||||||
|
const newAnnotsArray = pdfDoc.context.obj(annotsToKeep);
|
||||||
|
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
||||||
|
} else {
|
||||||
|
page.node.delete(PDFName.of('Annots'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function removeAnnotations() {
|
export async function removeAnnotations() {
|
||||||
showLoader('Removing annotations...');
|
showLoader('Removing annotations...');
|
||||||
try {
|
try {
|
||||||
const totalPages = state.pdfDoc.getPageCount();
|
const totalPages = state.pdfDoc.getPageCount();
|
||||||
let targetPageIndices = [];
|
let targetPageIndices = [];
|
||||||
|
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
const pageScope = (
|
||||||
const pageScope = document.querySelector('input[name="page-scope"]:checked').value;
|
document.querySelector(
|
||||||
|
'input[name="page-scope"]:checked'
|
||||||
|
) as HTMLInputElement
|
||||||
|
).value;
|
||||||
if (pageScope === 'all') {
|
if (pageScope === 'all') {
|
||||||
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
targetPageIndices = Array.from({ length: totalPages }, (_, i) => i);
|
||||||
} else {
|
} else {
|
||||||
@@ -46,7 +92,14 @@ export async function removeAnnotations() {
|
|||||||
const trimmedRange = range.trim();
|
const trimmedRange = range.trim();
|
||||||
if (trimmedRange.includes('-')) {
|
if (trimmedRange.includes('-')) {
|
||||||
const [start, end] = trimmedRange.split('-').map(Number);
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
if (isNaN(start) || isNaN(end) || start < 1 || end > totalPages || start > end) continue;
|
if (
|
||||||
|
isNaN(start) ||
|
||||||
|
isNaN(end) ||
|
||||||
|
start < 1 ||
|
||||||
|
end > totalPages ||
|
||||||
|
start > end
|
||||||
|
)
|
||||||
|
continue;
|
||||||
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
|
for (let i = start; i <= end; i++) targetPageIndices.push(i - 1);
|
||||||
} else {
|
} else {
|
||||||
const pageNum = Number(trimmedRange);
|
const pageNum = Number(trimmedRange);
|
||||||
@@ -57,47 +110,31 @@ export async function removeAnnotations() {
|
|||||||
targetPageIndices = [...new Set(targetPageIndices)];
|
targetPageIndices = [...new Set(targetPageIndices)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetPageIndices.length === 0) throw new Error('No valid pages were selected.');
|
if (targetPageIndices.length === 0)
|
||||||
|
throw new Error('No valid pages were selected.');
|
||||||
|
|
||||||
const typesToRemove = new Set(
|
const typesToRemove = new Set(
|
||||||
// @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'Element'.
|
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(
|
||||||
Array.from(document.querySelectorAll('.annot-checkbox:checked')).map(cb => cb.value)
|
(cb) => (cb as HTMLInputElement).value
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (typesToRemove.size === 0) throw new Error('Please select at least one annotation type to remove.');
|
if (typesToRemove.size === 0)
|
||||||
|
throw new Error('Please select at least one annotation type to remove.');
|
||||||
|
|
||||||
const pages = state.pdfDoc.getPages();
|
removeAnnotationsFromDoc(state.pdfDoc, targetPageIndices, typesToRemove);
|
||||||
|
|
||||||
for (const pageIndex of targetPageIndices) {
|
|
||||||
const page = pages[pageIndex];
|
|
||||||
const annotRefs = page.node.Annots()?.asArray() || [];
|
|
||||||
|
|
||||||
const annotsToKeep = [];
|
|
||||||
|
|
||||||
for (const ref of annotRefs) {
|
|
||||||
const annot = state.pdfDoc.context.lookup(ref);
|
|
||||||
const subtype = annot.get(PDFName.of('Subtype'))?.toString().substring(1);
|
|
||||||
|
|
||||||
// If the subtype is NOT in the list to remove, add it to our new array
|
|
||||||
if (!subtype || !typesToRemove.has(subtype)) {
|
|
||||||
annotsToKeep.push(ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (annotsToKeep.length > 0) {
|
|
||||||
const newAnnotsArray = state.pdfDoc.context.obj(annotsToKeep);
|
|
||||||
page.node.set(PDFName.of('Annots'), newAnnotsArray);
|
|
||||||
} else {
|
|
||||||
page.node.delete(PDFName.of('Annots'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'annotations-removed.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'annotations-removed.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', e.message || 'Could not remove annotations. Please check your page range.');
|
showAlert(
|
||||||
|
'Error',
|
||||||
|
e.message || 'Could not remove annotations. Please check your page range.'
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
hideLoader();
|
hideLoader();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
import * as pdfjsLib from 'pdfjs-dist';
|
||||||
|
import { PDFPageProxy } from 'pdfjs-dist/types/src/display/api.js';
|
||||||
|
|
||||||
|
let analysisCache = [];
|
||||||
|
|
||||||
|
async function isPageBlank(page: PDFPageProxy, threshold: number) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const viewport = page.getViewport({ scale: 0.2 });
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
|
||||||
|
await page.render({ canvasContext: context, viewport, canvas: canvas })
|
||||||
|
.promise;
|
||||||
|
|
||||||
|
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const data = imageData.data;
|
||||||
|
const totalPixels = data.length / 4;
|
||||||
|
let nonWhitePixels = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i += 4) {
|
||||||
|
if (data[i] < 245 || data[i + 1] < 245 || data[i + 2] < 245) {
|
||||||
|
nonWhitePixels++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blankness = 1 - nonWhitePixels / totalPixels;
|
||||||
|
return blankness >= threshold / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzePages() {
|
||||||
|
if (!state.pdfDoc) return;
|
||||||
|
showLoader('Analyzing for blank pages...');
|
||||||
|
|
||||||
|
const pdfBytes = await state.pdfDoc.save();
|
||||||
|
const pdf = await pdfjsLib.getDocument({ data: pdfBytes }).promise;
|
||||||
|
|
||||||
|
analysisCache = [];
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
promises.push(
|
||||||
|
pdf.getPage(i).then((page) =>
|
||||||
|
isPageBlank(page, 0).then((isActuallyBlank) => ({
|
||||||
|
pageNum: i,
|
||||||
|
isInitiallyBlank: isActuallyBlank,
|
||||||
|
pageRef: page,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
analysisCache = await Promise.all(promises);
|
||||||
|
hideLoader();
|
||||||
|
updateAnalysisUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateAnalysisUI() {
|
||||||
|
const sensitivity = parseInt(
|
||||||
|
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
(
|
||||||
|
document.getElementById('sensitivity-value') as HTMLSpanElement
|
||||||
|
).textContent = sensitivity.toString();
|
||||||
|
|
||||||
|
const previewContainer = document.getElementById('analysis-preview');
|
||||||
|
const analysisText = document.getElementById('analysis-text');
|
||||||
|
const thumbnailsContainer = document.getElementById(
|
||||||
|
'removed-pages-thumbnails'
|
||||||
|
);
|
||||||
|
|
||||||
|
thumbnailsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
const pagesToRemove = [];
|
||||||
|
|
||||||
|
for (const pageData of analysisCache) {
|
||||||
|
const isConsideredBlank = await isPageBlank(pageData.pageRef, sensitivity);
|
||||||
|
if (isConsideredBlank) {
|
||||||
|
pagesToRemove.push(pageData.pageNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pagesToRemove.length > 0) {
|
||||||
|
analysisText.textContent = `Found ${pagesToRemove.length} blank page(s) to remove: ${pagesToRemove.join(', ')}`;
|
||||||
|
previewContainer.classList.remove('hidden');
|
||||||
|
|
||||||
|
for (const pageNum of pagesToRemove) {
|
||||||
|
const pageData = analysisCache[pageNum - 1];
|
||||||
|
const viewport = pageData.pageRef.getViewport({ scale: 0.1 });
|
||||||
|
const thumbCanvas = document.createElement('canvas');
|
||||||
|
thumbCanvas.width = viewport.width;
|
||||||
|
thumbCanvas.height = viewport.height;
|
||||||
|
await pageData.pageRef.render({
|
||||||
|
canvasContext: thumbCanvas.getContext('2d'),
|
||||||
|
viewport,
|
||||||
|
}).promise;
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = thumbCanvas.toDataURL();
|
||||||
|
img.className = 'rounded border border-gray-600';
|
||||||
|
img.title = `Page ${pageNum}`;
|
||||||
|
thumbnailsContainer.appendChild(img);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
analysisText.textContent =
|
||||||
|
'No blank pages found at this sensitivity level.';
|
||||||
|
previewContainer.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupRemoveBlankPagesTool() {
|
||||||
|
await analyzePages();
|
||||||
|
document
|
||||||
|
.getElementById('sensitivity-slider')
|
||||||
|
.addEventListener('input', updateAnalysisUI);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeBlankPages() {
|
||||||
|
showLoader('Removing blank pages...');
|
||||||
|
try {
|
||||||
|
const sensitivity = parseInt(
|
||||||
|
(document.getElementById('sensitivity-slider') as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
const indicesToKeep = [];
|
||||||
|
|
||||||
|
for (const pageData of analysisCache) {
|
||||||
|
const isConsideredBlank = await isPageBlank(
|
||||||
|
pageData.pageRef,
|
||||||
|
sensitivity
|
||||||
|
);
|
||||||
|
if (!isConsideredBlank) {
|
||||||
|
indicesToKeep.push(pageData.pageNum - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indicesToKeep.length === 0) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'No Content Found',
|
||||||
|
'All pages were identified as blank at the current sensitivity setting. No new file was created. Try lowering the sensitivity if you believe this is an error.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indicesToKeep.length === state.pdfDoc.getPageCount()) {
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'No Pages Removed',
|
||||||
|
'No pages were identified as blank at the current sensitivity level.'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPdf = await PDFDocument.create();
|
||||||
|
const copiedPages = await newPdf.copyPages(state.pdfDoc, indicesToKeep);
|
||||||
|
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||||
|
|
||||||
|
const newPdfBytes = await newPdf.save();
|
||||||
|
downloadFile(
|
||||||
|
new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }),
|
||||||
|
'non-blank.pdf'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Error', 'Could not remove blank pages.');
|
||||||
|
} finally {
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,60 @@
|
|||||||
|
|
||||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
import { downloadFile } from '../utils/helpers.js';
|
import { downloadFile } from '../utils/helpers.js';
|
||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
import { PDFName } from 'pdf-lib';
|
||||||
|
|
||||||
export async function removeMetadata() {
|
export function removeMetadataFromDoc(pdfDoc) {
|
||||||
showLoader('Removing all metadata...');
|
const infoDict = pdfDoc.getInfoDict();
|
||||||
try {
|
|
||||||
const infoDict = state.pdfDoc.getInfoDict();
|
|
||||||
|
|
||||||
const allKeys = infoDict.keys();
|
const allKeys = infoDict.keys();
|
||||||
allKeys.forEach((key: any) => {
|
allKeys.forEach((key: any) => {
|
||||||
infoDict.delete(key);
|
infoDict.delete(key);
|
||||||
});
|
});
|
||||||
|
|
||||||
state.pdfDoc.setTitle('');
|
pdfDoc.setTitle('');
|
||||||
state.pdfDoc.setAuthor('');
|
pdfDoc.setAuthor('');
|
||||||
state.pdfDoc.setSubject('');
|
pdfDoc.setSubject('');
|
||||||
state.pdfDoc.setKeywords([]);
|
pdfDoc.setKeywords([]);
|
||||||
state.pdfDoc.setCreator('');
|
pdfDoc.setCreator('');
|
||||||
state.pdfDoc.setProducer('');
|
pdfDoc.setProducer('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
if (catalogDict.has(PDFName.of('Metadata'))) {
|
||||||
|
catalogDict.delete(PDFName.of('Metadata'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove XMP metadata:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const context = pdfDoc.context;
|
||||||
|
if (context.trailerInfo) {
|
||||||
|
delete context.trailerInfo.ID;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove document IDs:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const catalogDict = pdfDoc.catalog.dict;
|
||||||
|
if (catalogDict.has(PDFName.of('PieceInfo'))) {
|
||||||
|
catalogDict.delete(PDFName.of('PieceInfo'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not remove PieceInfo:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeMetadata() {
|
||||||
|
showLoader('Removing all metadata...');
|
||||||
|
try {
|
||||||
|
removeMetadataFromDoc(state.pdfDoc);
|
||||||
|
|
||||||
const newPdfBytes = await state.pdfDoc.save();
|
const newPdfBytes = await state.pdfDoc.save();
|
||||||
downloadFile(new Blob([newPdfBytes], { type: 'application/pdf' }), 'metadata-removed.pdf');
|
downloadFile(
|
||||||
|
new Blob([newPdfBytes], { type: 'application/pdf' }),
|
||||||
|
'metadata-removed.pdf'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
showAlert('Error', 'An error occurred while trying to remove metadata.');
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||||
|
import {
|
||||||
|
downloadFile,
|
||||||
|
initializeQpdf,
|
||||||
|
readFileAsArrayBuffer,
|
||||||
|
} from '../utils/helpers.js';
|
||||||
|
import { state } from '../state.js';
|
||||||
|
|
||||||
|
export async function removeRestrictions() {
|
||||||
|
const file = state.files[0];
|
||||||
|
const password =
|
||||||
|
(document.getElementById('owner-password-remove') as HTMLInputElement)
|
||||||
|
?.value || '';
|
||||||
|
|
||||||
|
const inputPath = '/input.pdf';
|
||||||
|
const outputPath = '/output.pdf';
|
||||||
|
let qpdf: any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
showLoader('Initializing...');
|
||||||
|
qpdf = await initializeQpdf();
|
||||||
|
|
||||||
|
showLoader('Reading PDF...');
|
||||||
|
const fileBuffer = await readFileAsArrayBuffer(file);
|
||||||
|
const uint8Array = new Uint8Array(fileBuffer as ArrayBuffer);
|
||||||
|
|
||||||
|
qpdf.FS.writeFile(inputPath, uint8Array);
|
||||||
|
|
||||||
|
showLoader('Removing restrictions...');
|
||||||
|
|
||||||
|
const args = [inputPath];
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
args.push(`--password=${password}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push('--decrypt', '--remove-restrictions', '--', outputPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
qpdf.callMain(args);
|
||||||
|
} catch (qpdfError: any) {
|
||||||
|
console.error('qpdf execution error:', qpdfError);
|
||||||
|
if (
|
||||||
|
qpdfError.message?.includes('password') ||
|
||||||
|
qpdfError.message?.includes('encrypt')
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'Failed to remove restrictions. The PDF may require the correct owner password.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
'Failed to remove restrictions: ' +
|
||||||
|
(qpdfError.message || 'Unknown error')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoader('Preparing download...');
|
||||||
|
const outputFile = qpdf.FS.readFile(outputPath, { encoding: 'binary' });
|
||||||
|
|
||||||
|
if (!outputFile || outputFile.length === 0) {
|
||||||
|
throw new Error('Operation resulted in an empty file.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([outputFile], { type: 'application/pdf' });
|
||||||
|
downloadFile(blob, `unrestricted-${file.name}`);
|
||||||
|
|
||||||
|
hideLoader();
|
||||||
|
|
||||||
|
showAlert(
|
||||||
|
'Success',
|
||||||
|
'PDF restrictions removed successfully! The file is now fully editable and printable.'
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error during restriction removal:', error);
|
||||||
|
hideLoader();
|
||||||
|
showAlert(
|
||||||
|
'Operation Failed',
|
||||||
|
`An error occurred: ${error.message || 'The PDF might be corrupted or password-protected.'}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (qpdf?.FS) {
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(inputPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to unlink input file:', e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
qpdf.FS.unlink(outputPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to unlink output file:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.warn('Failed to cleanup WASM FS:', cleanupError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,20 +3,40 @@ import { downloadFile } from '../utils/helpers.js';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
|
|
||||||
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
export async function reversePages() {
|
export async function reversePages() {
|
||||||
if (!state.pdfDoc) { showAlert('Error', 'PDF not loaded.'); return; }
|
const pdfDocs = state.files.filter(
|
||||||
|
(file: File) => file.type === 'application/pdf'
|
||||||
|
);
|
||||||
|
if (!pdfDocs.length) {
|
||||||
|
showAlert('Error', 'PDF not loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
showLoader('Reversing page order...');
|
showLoader('Reversing page order...');
|
||||||
try {
|
try {
|
||||||
|
const zip = new JSZip();
|
||||||
|
for (let j = 0; j < pdfDocs.length; j++) {
|
||||||
|
const file = pdfDocs[j];
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const pdfDoc = await PDFLibDocument.load(arrayBuffer);
|
||||||
const newPdf = await PDFLibDocument.create();
|
const newPdf = await PDFLibDocument.create();
|
||||||
const pageCount = state.pdfDoc.getPageCount();
|
const pageCount = pdfDoc.getPageCount();
|
||||||
const reversedIndices = Array.from({ length: pageCount }, (_, i) => pageCount - 1 - i);
|
const reversedIndices = Array.from(
|
||||||
|
{ length: pageCount },
|
||||||
|
(_, i) => pageCount - 1 - i
|
||||||
|
);
|
||||||
|
|
||||||
const copiedPages = await newPdf.copyPages(state.pdfDoc, reversedIndices);
|
const copiedPages = await newPdf.copyPages(pdfDoc, reversedIndices);
|
||||||
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
copiedPages.forEach((page: any) => newPdf.addPage(page));
|
||||||
|
|
||||||
const newPdfBytes = await newPdf.save();
|
const newPdfBytes = await newPdf.save();
|
||||||
downloadFile(new Blob([new Uint8Array(newPdfBytes)], { type: 'application/pdf' }), 'reversed.pdf');
|
const originalName = file.name.replace(/\.pdf$/i, '');
|
||||||
|
const fileName = `${originalName}_reversed.pdf`;
|
||||||
|
zip.file(fileName, newPdfBytes);
|
||||||
|
}
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
downloadFile(zipBlob, 'reversed_pdfs.zip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showAlert('Error', 'Could not reverse the PDF pages.');
|
showAlert('Error', 'Could not reverse the PDF pages.');
|
||||||
|
|||||||
Loaded 100 of 136 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user